diff --git a/python/lsst/ip/diffim/subtractImages.py b/python/lsst/ip/diffim/subtractImages.py index e0d998af6..3e4ad6e9b 100644 --- a/python/lsst/ip/diffim/subtractImages.py +++ b/python/lsst/ip/diffim/subtractImages.py @@ -26,7 +26,8 @@ import lsst.afw.image import lsst.afw.math import lsst.geom -from lsst.ip.diffim.utils import evaluateMeanPsfFwhm, getPsfFwhm, computeDifferenceImageMetrics, checkMask +from lsst.ip.diffim.utils import (evaluateMeanPsfFwhm, getPsfFwhm, computeDifferenceImageMetrics, + checkMask, setSourceFootprints) from lsst.meas.algorithms import ScaleVarianceTask, ScienceSourceSelectorTask import lsst.pex.config import lsst.pipe.base @@ -301,21 +302,29 @@ def setDefaults(self): self.makeKernel.kernel.active.fitForBackground = True self.makeKernel.kernel.active.spatialKernelOrder = 1 self.makeKernel.kernel.active.spatialBgOrder = 2 - self.sourceSelector.doUnresolved = True # apply star-galaxy separation + # Shared source selector settings + doSkySources = False # Do not include sky sources + doSignalToNoise = True # apply signal to noise filter + doUnresolved = True # apply star-galaxy separation + signalToNoiseMinimum = 10 + signalToNoiseMaximum = 500 self.sourceSelector.doIsolated = True # apply isolated star selection self.sourceSelector.doRequirePrimary = True # apply primary flag selection - self.sourceSelector.doSkySources = False # Do not include sky sources - self.sourceSelector.doSignalToNoise = True # apply signal to noise filter - self.sourceSelector.signalToNoise.minimum = 10 - self.sourceSelector.signalToNoise.maximum = 500 - self.fallbackSourceSelector.doSkySources = False # Do not include sky sources - self.fallbackSourceSelector.doSignalToNoise = True # apply signal to noise filter - self.fallbackSourceSelector.signalToNoise.minimum = 10 + self.sourceSelector.doUnresolved = doUnresolved + self.sourceSelector.doSkySources = doSkySources + self.sourceSelector.doSignalToNoise = doSignalToNoise + self.sourceSelector.signalToNoise.minimum = signalToNoiseMinimum + self.sourceSelector.signalToNoise.maximum = signalToNoiseMaximum # The following two configs should not be necessary to be turned on for # PSF-matching, and the fallback kernel source selection will fail if # they are set since it does not run deblending. self.fallbackSourceSelector.doIsolated = False # Do not apply isolated star selection self.fallbackSourceSelector.doRequirePrimary = False # Do not apply primary flag selection + self.fallbackSourceSelector.doUnresolved = doUnresolved + self.fallbackSourceSelector.doSkySources = doSkySources + self.fallbackSourceSelector.doSignalToNoise = doSignalToNoise + self.fallbackSourceSelector.signalToNoise.minimum = signalToNoiseMinimum + self.fallbackSourceSelector.signalToNoise.maximum = signalToNoiseMaximum class AlardLuptonSubtractConfig(AlardLuptonSubtractBaseConfig, lsst.pipe.base.PipelineTaskConfig, @@ -427,25 +436,15 @@ def run(self, template, science, sources, visitSummary=None): ``kernelSources` : `lsst.afw.table.SourceCatalog` Sources from the input catalog that were used to construct the PSF-matching kernel. - - Raises - ------ - RuntimeError - If an unsupported convolution mode is supplied. - RuntimeError - If there are too few sources to calculate the PSF matching kernel. - lsst.pipe.base.NoWorkFound - Raised if fraction of good pixels, defined as not having NO_DATA - set, is less then the configured requiredTemplateFraction """ self._prepareInputs(template, science, visitSummary=visitSummary) convolveTemplate = self.chooseConvolutionMethod(template, science) - selectSources = self._sourceSelector(sources, science.getBBox(), template.mask) + kernelResult = self.runMakeKernel(template, science, sources=sources, + convolveTemplate=convolveTemplate, + runSourceDetection=False) - kernelResult = self.runMakeKernel(template, science, selectSources, - convolveTemplate=convolveTemplate) if self.config.doSubtractBackground: backgroundModel = kernelResult.backgroundModel else: @@ -515,7 +514,7 @@ def chooseConvolutionMethod(self, template, science): raise RuntimeError("Cannot handle AlardLuptonSubtract mode: %s", self.config.mode) return convolveTemplate - def runMakeKernel(self, template, science, sources, convolveTemplate=True): + def runMakeKernel(self, template, science, sources=None, convolveTemplate=True, runSourceDetection=False): """Construct the PSF-matching kernel. Parameters @@ -528,8 +527,13 @@ def runMakeKernel(self, template, science, sources, convolveTemplate=True): Identified sources on the science exposure. This catalog is used to select sources in order to perform the AL PSF matching on stamp images around them. + Not used if ``runSourceDetection`` is set. convolveTemplate : `bool`, optional Construct the matching kernel to convolve the template? + runSourceDetection : `bool`, optional + Run a minimal version of source detection to determine kernel + candidates? If False, a source list to select kernel candidates + from must be supplied. Returns ------- @@ -543,6 +547,7 @@ def runMakeKernel(self, template, science, sources, convolveTemplate=True): Sources from the input catalog that were used to construct the PSF-matching kernel. """ + if convolveTemplate: reference = template target = science @@ -554,35 +559,14 @@ def runMakeKernel(self, template, science, sources, convolveTemplate=True): referenceFwhmPix = self.sciencePsfSize targetFwhmPix = self.templatePsfSize try: - # The outer try..except block catches any error, and raises - # NoWorkFound if the template coverage is insufficient. Otherwise, - # the original error is raised. - try: - # The inner try..except block catches errors related to the - # input source catalog. If the catalog is not sufficient to - # constrain the kernel fit and `allowKernelSourceDetection=True` - # then source detection and measurement are re-run to make a new - # catalog. Otherwise, the original error is raised. - kernelSources = self.makeKernel.selectKernelSources(reference, target, - candidateList=sources, - preconvolved=False, - templateFwhmPix=referenceFwhmPix, - scienceFwhmPix=targetFwhmPix) - kernelResult = self.makeKernel.run(reference, target, kernelSources, - preconvolved=False, - templateFwhmPix=referenceFwhmPix, - scienceFwhmPix=targetFwhmPix) - except Exception as e: - if self.config.allowKernelSourceDetection and convolveTemplate: - self.log.warning("Error encountered trying to construct the matching kernel" - f" Running source detection and retrying. {e}") - kernelSources = self.runKernelSourceDetection(template, science) - kernelResult = self.makeKernel.run(reference, target, kernelSources, - preconvolved=False, - templateFwhmPix=referenceFwhmPix, - scienceFwhmPix=targetFwhmPix) - else: - raise e + if runSourceDetection: + kernelSources = self.runKernelSourceDetection(template, science) + else: + kernelSources = self._sourceSelector(template, science, sources) + kernelResult = self.makeKernel.run(reference, target, kernelSources, + preconvolved=False, + templateFwhmPix=referenceFwhmPix, + scienceFwhmPix=targetFwhmPix) except (RuntimeError, lsst.pex.exceptions.Exception) as e: self.log.warning("Failed to match template. Checking coverage") # Raise NoWorkFound if template fraction is insufficient @@ -592,6 +576,7 @@ def runMakeKernel(self, template, science, sources, convolveTemplate=True): f" Failure is tolerable: {e}") # checkTemplateIsSufficient did not raise NoWorkFound, so raise original exception raise e + return lsst.pipe.base.Struct(backgroundModel=kernelResult.backgroundModel, psfMatchingKernel=kernelResult.psfMatchingKernel, kernelSources=kernelSources) @@ -626,7 +611,7 @@ def runKernelSourceDetection(self, template, science): scienceFwhmPix=self.sciencePsfSize) # return sources - return self._sourceSelector(sources, science.getBBox(), template.mask, fallback=True) + return self._sourceSelector(template, science, sources, fallback=True) def runConvolveTemplate(self, template, science, psfMatchingKernel, backgroundModel=None): """Convolve the template image with a PSF-matching kernel and subtract @@ -638,10 +623,11 @@ def runConvolveTemplate(self, template, science, psfMatchingKernel, backgroundMo Template exposure, warped to match the science exposure. science : `lsst.afw.image.ExposureF` Science exposure to subtract from the template. - selectSources : `lsst.afw.table.SourceCatalog` - Identified sources on the science exposure. This catalog is used to - select sources in order to perform the AL PSF matching on stamp - images around them. + psfMatchingKernel : `lsst.afw.math.Kernel` + Kernel to be used to PSF-match the science image to the template. + backgroundModel : `lsst.afw.math.Function2D`, optional + Background model that was fit while solving for the PSF-matching + kernel. Returns ------- @@ -685,10 +671,11 @@ def runConvolveScience(self, template, science, psfMatchingKernel, backgroundMod Template exposure, warped to match the science exposure. science : `lsst.afw.image.ExposureF` Science exposure to subtract from the template. - selectSources : `lsst.afw.table.SourceCatalog` - Identified sources on the science exposure. This catalog is used to - select sources in order to perform the AL PSF matching on stamp - images around them. + psfMatchingKernel : `lsst.afw.math.Kernel` + Kernel to be used to PSF-match the science image to the template. + backgroundModel : `lsst.afw.math.Function2D`, optional + Background model that was fit while solving for the PSF-matching + kernel. Returns ------- @@ -884,6 +871,9 @@ def _convolveExposure(self, exposure, kernel, convolutionControl, Point spread function (PSF) to set for the convolved exposure. photoCalib : `lsst.afw.image.PhotoCalib`, optional Photometric calibration of the convolved exposure. + interpolateBadMaskPlanes : `bool`, optional + If set, interpolate over mask planes specified in + ``config.badMaskPlanes`` before convolving the image. Returns ------- @@ -907,78 +897,88 @@ def _convolveExposure(self, exposure, kernel, convolutionControl, else: return convolvedExposure[bbox] - def _sourceSelector(self, sources, bbox, mask, fallback=False): + def _sourceSelector(self, template, science, sources, fallback=False, preconvolved=False): """Select sources from a catalog that meet the selection criteria. The selection criteria include any configured parameters of the - `sourceSelector` subtask, as well as distance from the edge if - `restrictKernelEdgeSources` is set. + `sourceSelector` subtask, as well as checking the science and template + mask planes. Parameters ---------- + template : `lsst.afw.image.ExposureF` + Template exposure, warped to match the science exposure. + science : `lsst.afw.image.ExposureF` + Science exposure to subtract from the template. sources : `lsst.afw.table.SourceCatalog` Input source catalog to select sources from. - bbox : `lsst.geom.Box2I` - Bounding box of the science image. - mask : `lsst.afw.image.Mask` - The mask plane of the template to use to reject kernel candidates. fallback : `bool`, optional Switch indicating the source selector is being called after running the fallback source detection subtask, which does not run a full set of measurement plugins and can't use the same settings for the source selector. + preconvolved : `bool`, optional + If set, exclude a wider buffer around the edge of the image to + account for an extra convolution. Returns ------- - selectSources : `lsst.afw.table.SourceCatalog` + kernelSources : `lsst.afw.table.SourceCatalog` The input source catalog, with flagged and low signal-to-noise - sources removed. + sources removed and footprints added. Raises ------ InsufficientKernelSourcesError An AlgorithmError that is raised if there are not enough PSF candidates to construct the PSF matching kernel. - RuntimeError - If there are too few sources to compute the PSF matching kernel - remaining after source selection. """ if fallback: selected = self.fallbackSourceSelector.selectSources(sources).selected else: selected = self.sourceSelector.selectSources(sources).selected - if self.config.restrictKernelEdgeSources: - rejectRadius = 2*self.config.makeKernel.kernel.active.kernelSize - bbox.grow(-rejectRadius) - bboxSelected = bbox.contains(sources.getX(), sources.getY()) - self.log.info("Rejecting %i candidate sources within %i pixels of the edge.", - np.count_nonzero(~bboxSelected), rejectRadius) - selected &= bboxSelected + sciencePsfSize = self.sciencePsfSize*np.sqrt(2) if preconvolved else self.sciencePsfSize + kSize = self.makeKernel.makeKernelBasisList(self.templatePsfSize, sciencePsfSize)[0].getWidth() selectSources = sources[selected].copy(deep=True) - # Optionally remove sources that land on masked pixels - maskSelected = checkMask(mask, selectSources, self.config.excludeMaskPlanes, checkAdjacent=True) - selectSources = selectSources[maskSelected].copy(deep=True) - # Trim selectSources if they exceed ``maxKernelSources``. + # Set the footprints, to be used in `makeKernel` and `checkMask`. + kernelSources = setSourceFootprints(selectSources, kernelSize=kSize) + bbox = science.getBBox() + if preconvolved: + bbox.grow(-kSize) + if self.config.restrictKernelEdgeSources: + bbox.grow(-kSize) + # Remove sources that land on masked pixels + scienceSelected = checkMask(science.mask[bbox], kernelSources, self.config.excludeMaskPlanes) + templateSelected = checkMask(template.mask[bbox], kernelSources, self.config.excludeMaskPlanes) + maskSelected = scienceSelected & templateSelected + kernelSources = kernelSources[maskSelected].copy(deep=True) + # Trim kernelSources if they exceed ``maxKernelSources``. # Keep the highest signal-to-noise sources of those selected. - if (len(selectSources) > self.config.maxKernelSources) & (self.config.maxKernelSources > 0): - signalToNoise = selectSources.getPsfInstFlux()/selectSources.getPsfInstFluxErr() + if (len(kernelSources) > self.config.maxKernelSources) & (self.config.maxKernelSources > 0): + signalToNoise = kernelSources.getPsfInstFlux()/kernelSources.getPsfInstFluxErr() indices = np.argsort(signalToNoise) indices = indices[-self.config.maxKernelSources:] - selected = np.zeros(len(selectSources), dtype=bool) + selected = np.zeros(len(kernelSources), dtype=bool) selected[indices] = True - selectSources = selectSources[selected].copy(deep=True) + kernelSources = kernelSources[selected].copy(deep=True) self.log.info("%i/%i=%.1f%% of sources selected for PSF matching from the input catalog", - len(selectSources), len(sources), 100*len(selectSources)/len(sources)) - if len(selectSources) < self.config.minKernelSources: + len(kernelSources), len(sources), 100*len(kernelSources)/len(sources)) + if len(kernelSources) < self.config.minKernelSources: self.log.error("Too few sources to calculate the PSF matching kernel: " "%i selected but %i needed for the calculation.", - len(selectSources), self.config.minKernelSources) - if not self.config.allowKernelSourceDetection: - raise InsufficientKernelSourcesError(nSources=len(selectSources), + len(kernelSources), self.config.minKernelSources) + if self.config.allowKernelSourceDetection and not fallback: + # The fallback source detection pipeline calls this method, so + # allowing source detection in that case would create an endless + # loop + kernelSources = self.runKernelSourceDetection(template, science) + else: + raise InsufficientKernelSourcesError(nSources=len(kernelSources), nRequired=self.config.minKernelSources) - self.metadata["nPsfSources"] = len(selectSources) - return selectSources + self.metadata["nPsfSources"] = len(kernelSources) + + return kernelSources def _prepareInputs(self, template, science, visitSummary=None): """Perform preparatory calculations common to all Alard&Lupton Tasks. @@ -1223,9 +1223,9 @@ def run(self, template, science, sources, visitSummary=None): interpolateBadMaskPlanes=True) self.metadata["convolvedExposure"] = "Preconvolution" try: - selectSources = self._sourceSelector(sources, science.getBBox(), template.mask) + kernelSources = self._sourceSelector(template, matchedScience, sources, preconvolved=True) subtractResults = self.runPreconvolve(template, science, matchedScience, - selectSources, scienceKernel) + kernelSources, scienceKernel) except (RuntimeError, lsst.pex.exceptions.Exception) as e: self.log.warning("Failed to match template. Checking coverage") @@ -1239,7 +1239,7 @@ def run(self, template, science, sources, visitSummary=None): return subtractResults - def runPreconvolve(self, template, science, matchedScience, selectSources, preConvKernel): + def runPreconvolve(self, template, science, matchedScience, kernelSources, preConvKernel): """Convolve the science image with its own PSF, then convolve the template with a matching kernel and subtract to form the Score exposure. @@ -1252,7 +1252,7 @@ def runPreconvolve(self, template, science, matchedScience, selectSources, preCo Science exposure to subtract from the template. matchedScience : `lsst.afw.image.ExposureF` The science exposure, convolved with the reflection of its own PSF. - selectSources : `lsst.afw.table.SourceCatalog` + kernelSources : `lsst.afw.table.SourceCatalog` Identified sources on the science exposure. This catalog is used to select sources in order to perform the AL PSF matching on stamp images around them. @@ -1283,11 +1283,6 @@ def runPreconvolve(self, template, science, matchedScience, selectSources, preCo bbox = science.getBBox() innerBBox = preConvKernel.shrinkBBox(bbox) - kernelSources = self.makeKernel.selectKernelSources(template[innerBBox], matchedScience[innerBBox], - candidateList=selectSources, - preconvolved=True, - templateFwhmPix=self.templatePsfSize, - scienceFwhmPix=self.sciencePsfSize) kernelResult = self.makeKernel.run(template[innerBBox], matchedScience[innerBBox], kernelSources, preconvolved=True, templateFwhmPix=self.templatePsfSize, @@ -1467,6 +1462,9 @@ def run(self, template, science, visitSummary=None, inputPsfMatchingKernel=None) Exposure catalog with external calibrations to be applied. Catalog uses the detector id for the catalog id, sorted on id for fast lookup. + inputPsfMatchingKernel : `lsst.afw.math.Kernel`, optional + Pre-existing PSF matching kernel to use for convolution. + Required, and only used, if ``config.useExistingKernel`` is set. Returns ------- @@ -1486,10 +1484,6 @@ def run(self, template, science, visitSummary=None, inputPsfMatchingKernel=None) Raises ------ - RuntimeError - If an unsupported convolution mode is supplied. - RuntimeError - If there are too few sources to calculate the PSF matching kernel. lsst.pipe.base.NoWorkFound Raised if fraction of good pixels, defined as not having NO_DATA set, is less then the configured requiredTemplateFraction @@ -1503,7 +1497,8 @@ def run(self, template, science, visitSummary=None, inputPsfMatchingKernel=None) backgroundModel = None kernelSources = None else: - kernelResult = self.runMakeKernel(template, science, convolveTemplate=convolveTemplate) + kernelResult = self.runMakeKernel(template, science, convolveTemplate=convolveTemplate, + runSourceDetection=True) psfMatchingKernel = kernelResult.psfMatchingKernel kernelSources = kernelResult.kernelSources if self.config.doSubtractBackground: @@ -1520,66 +1515,6 @@ def run(self, template, science, visitSummary=None, inputPsfMatchingKernel=None) subtractResults.kernelSources = kernelSources return subtractResults - def runMakeKernel(self, template, science, convolveTemplate=True): - """Construct the PSF-matching kernel. - - Parameters - ---------- - template : `lsst.afw.image.ExposureF` - Template exposure, warped to match the science exposure. - science : `lsst.afw.image.ExposureF` - Science exposure to subtract from the template. - sources : `lsst.afw.table.SourceCatalog` - Identified sources on the science exposure. This catalog is used to - select sources in order to perform the AL PSF matching on stamp - images around them. - convolveTemplate : `bool`, optional - Construct the matching kernel to convolve the template? - - Returns - ------- - results : `lsst.pipe.base.Struct` - ``backgroundModel`` : `lsst.afw.math.Function2D` - Background model that was fit while solving for the - PSF-matching kernel - ``psfMatchingKernel`` : `lsst.afw.math.Kernel` - Kernel used to PSF-match the convolved image. - ``kernelSources` : `lsst.afw.table.SourceCatalog` - Sources from the input catalog that were used to construct the - PSF-matching kernel. - """ - if convolveTemplate: - reference = template - target = science - referenceFwhmPix = self.templatePsfSize - targetFwhmPix = self.sciencePsfSize - else: - reference = science - target = template - referenceFwhmPix = self.sciencePsfSize - targetFwhmPix = self.templatePsfSize - try: - # The try..except block catches any error, and raises - # NoWorkFound if the template coverage is insufficient. Otherwise, - # the original error is raised. - kernelSources = self.runKernelSourceDetection(template, science) - kernelResult = self.makeKernel.run(reference, target, kernelSources, - preconvolved=False, - templateFwhmPix=referenceFwhmPix, - scienceFwhmPix=targetFwhmPix) - except (RuntimeError, lsst.pex.exceptions.Exception) as e: - self.log.warning("Failed to match template. Checking coverage") - # Raise NoWorkFound if template fraction is insufficient - checkTemplateIsSufficient(template[science.getBBox()], science, self.log, - self.config.minTemplateFractionForExpectedSuccess, - exceptionMessage="Template coverage lower than expected to succeed." - f" Failure is tolerable: {e}") - # checkTemplateIsSufficient did not raise NoWorkFound, so raise original exception - raise e - return lsst.pipe.base.Struct(backgroundModel=kernelResult.backgroundModel, - psfMatchingKernel=kernelResult.psfMatchingKernel, - kernelSources=kernelSources) - def _interpolateImage(maskedImage, badMaskPlanes, fallbackValue=None): """Replace masked image pixels with interpolated values. diff --git a/python/lsst/ip/diffim/utils.py b/python/lsst/ip/diffim/utils.py index 466fd6e1e..dc8d28fc3 100644 --- a/python/lsst/ip/diffim/utils.py +++ b/python/lsst/ip/diffim/utils.py @@ -23,7 +23,7 @@ __all__ = ["evaluateMeanPsfFwhm", "getPsfFwhm", "getKernelCenterDisplacement", - "computeDifferenceImageMetrics", "checkMask" + "computeDifferenceImageMetrics", "checkMask", "setSourceFootprints", ] import itertools @@ -456,8 +456,8 @@ def populate_sattle_visit_cache(visit_info, historical=False): r.raise_for_status() -def checkMask(mask, sources, excludeMaskPlanes, checkAdjacent=True): - """Exclude sources that are located on or adjacent to masked pixels. +def checkMask(mask, sources, excludeMaskPlanes): + """Exclude sources that have masked pixels in their footprints. Parameters ---------- @@ -471,7 +471,7 @@ def checkMask(mask, sources, excludeMaskPlanes, checkAdjacent=True): Returns ------- - flags : `numpy.ndarray` of `bool` + good : `numpy.ndarray` of `bool` Array indicating whether each source in the catalog should be kept (True) or rejected (False) based on the value of the mask plane at its location. @@ -482,16 +482,41 @@ def checkMask(mask, sources, excludeMaskPlanes, checkAdjacent=True): excludePixelMask = mask.getPlaneBitMask(setExcludeMaskPlanes) - xv = (np.rint(sources.getX() - mask.getX0())).astype(int) - yv = (np.rint(sources.getY() - mask.getY0())).astype(int) + good = np.ones(len(sources), dtype=bool) + for i, source in enumerate(sources): + bbox = source.getFootprint().getBBox() + if not mask.getBBox().contains(bbox): + good[i] = False + continue - flags = np.ones(len(sources), dtype=bool) - if checkAdjacent: - pixRange = (0, -1, 1) - else: - pixRange = (0,) - for j in pixRange: - for i in pixRange: - mv = mask.array[yv + j, xv + i] - flags *= np.bitwise_and(mv, excludePixelMask) == 0 - return flags + # Reject footprints with any bad mask bits set. + if (mask.subset(bbox).array & excludePixelMask).any(): + good[i] = False + continue + return good + + +def setSourceFootprints(sources, kernelSize): + """Add footprints of fixed size to a source catalog + + Parameters + ---------- + sources : `lsst.afw.table.SourceCatalog` + The source catalog to add footprints to. + kernelSize : `int` + The "radius" of the footprint, i.e half the size of the bounding box. + + Returns + ------- + sources : `lsst.afw.table.SourceCatalog` + The modified source catalog + """ + size = 2*kernelSize + 1 + for source in sources: + bbox = lsst.geom.Box2I.makeCenteredBox(source.getCentroid(), + lsst.geom.Extent2I(size, size)) + peak = source.getFootprint().getPeaks()[0] + boxFootprint = lsst.afw.detection.Footprint(lsst.afw.geom.SpanSet(bbox)) + boxFootprint.addPeak(peak.getFx(), peak.getFy(), peak.getPeakValue()) + source.setFootprint(boxFootprint) + return sources diff --git a/tests/test_subtractTask.py b/tests/test_subtractTask.py index 15f19eb2f..9052f719f 100644 --- a/tests/test_subtractTask.py +++ b/tests/test_subtractTask.py @@ -59,6 +59,7 @@ def _setup_subtraction(self, fluxField="truth_instFlux", errField="truth_instFlu """ config = self.subtractTask.ConfigClass() config.doSubtractBackground = False + config.restrictKernelEdgeSources = False config.sourceSelector.signalToNoise.fluxField = fluxField config.sourceSelector.signalToNoise.errField = errField config.sourceSelector.doUnresolved = True @@ -117,9 +118,10 @@ def test_incomplete_template_coverage(self): border = 20 xSize = 400 ySize = 400 - science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6, nSrc=100, + nSources = 80 + science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6, nSrc=nSources, xSize=xSize, ySize=ySize) - template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7, nSrc=100, + template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7, nSrc=nSources, templateBorderSize=border, doApplyCalibration=True, xSize=xSize, ySize=ySize) @@ -449,19 +451,21 @@ def test_kernel_source_selector(self): xSize = 256 ySize = 256 nSourcesSimulated = 20 - science, sources = makeTestImage(psfSize=2.4, nSrc=nSourcesSimulated, + sciencePsfSize = 2.4 + templatePsfSize = 2.0 + science, sources = makeTestImage(psfSize=sciencePsfSize, nSrc=nSourcesSimulated, xSize=xSize, ySize=ySize) - template, _ = makeTestImage(psfSize=2.0, nSrc=nSourcesSimulated, + template, _ = makeTestImage(psfSize=templatePsfSize, nSrc=nSourcesSimulated, xSize=xSize, ySize=ySize, doApplyCalibration=True) - def _run_and_check_sources(sourcesIn, maxKernelSources=1000, minKernelSources=3, - restrictKernelEdgeSources=False): + def _run_and_check_sources(sourcesIn, maxKernelSources=1000, minKernelSources=3): sources = sourcesIn.copy(deep=True) task = self._setup_subtraction(maxKernelSources=maxKernelSources, minKernelSources=minKernelSources, - restrictKernelEdgeSources=restrictKernelEdgeSources, ) + task.templatePsfSize = templatePsfSize + task.sciencePsfSize = sciencePsfSize # Verify that source flags are not set in the input catalog # Note that this will use the last flag in the list for the rest of # the test. @@ -470,12 +474,11 @@ def _run_and_check_sources(sourcesIn, maxKernelSources=1000, minKernelSources=3, nSources = len(sources) # Flag a third of the sources sources[0:: 3][badSourceFlag] = True - if restrictKernelEdgeSources: - rejectRadius = 2*task.config.makeKernel.kernel.active.kernelSize - bbox = science.getBBox() - bbox.grow(-rejectRadius) - edgeSources = ~bbox.contains(sources.getX(), sources.getY()) - sources[edgeSources][badSourceFlag] = True + rejectRadius = 2*task.config.makeKernel.kernel.active.kernelSize + bbox = science.getBBox() + bbox.grow(-rejectRadius) + edgeSources = ~bbox.contains(sources.getX(), sources.getY()) + sources[edgeSources][badSourceFlag] = True nBadSources = np.sum(sources[badSourceFlag]) if maxKernelSources > 0: nGoodSources = np.minimum(nSources - nBadSources, maxKernelSources) @@ -485,7 +488,7 @@ def _run_and_check_sources(sourcesIn, maxKernelSources=1000, minKernelSources=3, signalToNoise = sources.getPsfInstFlux()/sources.getPsfInstFluxErr() signalToNoise = signalToNoise[~sources[badSourceFlag]] signalToNoise.sort() - selectSources = task._sourceSelector(sources, science.getBBox(), template.mask) + selectSources = task._sourceSelector(template, science, sources) self.assertEqual(nGoodSources, len(selectSources)) signalToNoiseOut = selectSources.getPsfInstFlux()/selectSources.getPsfInstFluxErr() signalToNoiseOut.sort() @@ -493,8 +496,6 @@ def _run_and_check_sources(sourcesIn, maxKernelSources=1000, minKernelSources=3, _run_and_check_sources(sources) _run_and_check_sources(sources, maxKernelSources=len(sources)//3) - _run_and_check_sources(sources, restrictKernelEdgeSources=True) - _run_and_check_sources(sources, maxKernelSources=len(sources)//3, restrictKernelEdgeSources=True) _run_and_check_sources(sources, maxKernelSources=-1) with self.assertRaises(RuntimeError): _run_and_check_sources(sources, minKernelSources=1000) @@ -1215,7 +1216,7 @@ def test_background_subtraction(self): statsCtrl, statistic=afwMath.STDEV) # get the img psf Noise Equivalent Area value nea = computePSFNoiseEquivalentArea(science.psf) - self.assertFloatsAlmostEqual(stdVal, np.sqrt(2)*noiseLevel/np.sqrt(nea), rtol=0.1) + self.assertFloatsAlmostEqual(stdVal, np.sqrt(2)*noiseLevel/np.sqrt(nea), rtol=0.12) def test_scale_variance(self): """Check variance scaling of the Score image.