Skip to content

Commit c37df83

Browse files
authored
Merge pull request #431 from lsst/tickets/DM-52420
DM-52420: Add flag for diaSources in high variance regions
2 parents e65e943 + 5475126 commit c37df83

5 files changed

Lines changed: 117 additions & 17 deletions

File tree

python/lsst/ip/diffim/detectAndMeasure.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -416,9 +416,9 @@ def setDefaults(self):
416416

417417
# Keep track of which footprints contain streaks
418418
self.measurement.plugins["base_PixelFlags"].masksFpAnywhere = [
419-
"STREAK", "INJECTED", "INJECTED_TEMPLATE"]
419+
"STREAK", "INJECTED", "INJECTED_TEMPLATE", "HIGH_VARIANCE"]
420420
self.measurement.plugins["base_PixelFlags"].masksFpCenter = [
421-
"STREAK", "INJECTED", "INJECTED_TEMPLATE"]
421+
"STREAK", "INJECTED", "INJECTED_TEMPLATE", "HIGH_VARIANCE"]
422422
self.skySources.avoidMask = ["DETECTED", "DETECTED_NEGATIVE", "BAD", "NO_DATA", "EDGE"]
423423

424424
def validate(self):
@@ -808,7 +808,10 @@ def processResults(self, science, matchedTemplate, difference, sources, idFactor
808808
# This option allows returning sky sources,
809809
# even if there are no diaSources
810810
measurementResults.diaSources = diaSources
811-
811+
self.log.info("Measured %d diaSources and %d sky sources",
812+
np.count_nonzero(~diaSources["sky_source"]),
813+
np.count_nonzero(diaSources["sky_source"])
814+
)
812815
return measurementResults
813816

814817
def _deblend(self, difference, positiveFootprints, negativeFootprints):

python/lsst/ip/diffim/getTemplate.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434

3535
from lsst.skymap import BaseSkyMap
3636
from lsst.ip.diffim.dcrModel import DcrModel
37-
from lsst.meas.algorithms import CoaddPsf, CoaddPsfConfig
37+
from lsst.meas.algorithms import CoaddPsf, CoaddPsfConfig, SubtractBackgroundTask
3838
from lsst.utils.timer import timeMethod
3939

4040
__all__ = [
@@ -103,6 +103,17 @@ class GetTemplateConfig(
103103
doc="Configuration for CoaddPsf",
104104
dtype=CoaddPsfConfig,
105105
)
106+
varianceBackground = pexConfig.ConfigurableField(
107+
target=SubtractBackgroundTask,
108+
doc="Task to estimate the background variance.",
109+
)
110+
highVarianceThreshold = pexConfig.RangeField(
111+
dtype=float,
112+
default=4,
113+
min=1,
114+
doc="Set the HIGH_VARIANCE mask plane for regions with variance"
115+
" greater than the median by this factor.",
116+
)
106117

107118
def setDefaults(self):
108119
# Use a smaller cache: per SeparableKernel.computeCache, this should
@@ -115,6 +126,19 @@ def setDefaults(self):
115126
self.warp.warpingKernelName = "lanczos3"
116127
self.coaddPsf.warpingKernelName = self.warp.warpingKernelName
117128

129+
# Background subtraction of the variance plane
130+
self.varianceBackground.algorithm = "LINEAR"
131+
self.varianceBackground.binSize = 32
132+
self.varianceBackground.useApprox = False
133+
self.varianceBackground.statisticsProperty = "MEDIAN"
134+
self.varianceBackground.doFilterSuperPixels = True
135+
self.varianceBackground.ignoredPixelMask = ["BAD",
136+
"EDGE",
137+
"DETECTED",
138+
"DETECTED_NEGATIVE",
139+
"NO_DATA",
140+
]
141+
118142

119143
class GetTemplateTask(pipeBase.PipelineTask):
120144
ConfigClass = GetTemplateConfig
@@ -137,6 +161,7 @@ def __init__(self, *args, **kwargs):
137161
type=float,
138162
doc="Weight for each exposure, used to make the CoaddPsf; should always be 1.",
139163
)
164+
self.makeSubtask("varianceBackground")
140165

141166
def runQuantum(self, butlerQC, inputRefs, outputRefs):
142167
inputs = butlerQC.get(inputRefs)
@@ -357,11 +382,30 @@ def run(self, *, coaddExposureHandles, bbox, wcs, dataIds, physical_filter):
357382
for c in catalogs:
358383
catalog.extend(c)
359384

385+
# Set a mask plane for any regions with exceptionally high variance.
386+
self.checkHighVariance(template)
360387
template.setPsf(self._makePsf(template, catalog, wcs))
361388
template.setFilter(afwImage.FilterLabel(band, physical_filter))
362389
template.setPhotoCalib(photoCalib)
363390
return pipeBase.Struct(template=template)
364391

392+
def checkHighVariance(self, template):
393+
"""Set a mask plane for regions with unusually high variance.
394+
395+
Parameters
396+
----------
397+
template : `lsst.afw.image.Exposure`
398+
The warped template exposure, which will be modified in place.
399+
"""
400+
highVarianceMaskPlaneBit = template.mask.addMaskPlane("HIGH_VARIANCE")
401+
template.mask.getPlaneBitMask("HIGH_VARIANCE")
402+
varianceExposure = template.clone()
403+
varianceExposure.image.array = varianceExposure.variance.array
404+
varianceBackground = self.varianceBackground.run(varianceExposure).background.getImage().array
405+
threshold = self.config.highVarianceThreshold*np.nanmedian(varianceBackground)
406+
highVariancePix = varianceBackground > threshold
407+
template.mask.array[highVariancePix] |= 2**highVarianceMaskPlaneBit
408+
365409
@staticmethod
366410
def _checkInputs(dataIds, coaddExposures):
367411
"""Check that the all the dataIds are from the same band and that

python/lsst/ip/diffim/subtractImages.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
import lsst.afw.image
2727
import lsst.afw.math
2828
import lsst.geom
29-
from lsst.ip.diffim.utils import evaluateMeanPsfFwhm, getPsfFwhm, computeDifferenceImageMetrics
29+
from lsst.ip.diffim.utils import evaluateMeanPsfFwhm, getPsfFwhm, computeDifferenceImageMetrics, checkMask
3030
from lsst.meas.algorithms import ScaleVarianceTask, ScienceSourceSelectorTask
3131
import lsst.pex.config
3232
import lsst.pipe.base
@@ -268,9 +268,8 @@ class AlardLuptonSubtractBaseConfig(lsst.pex.config.Config):
268268
)
269269
excludeMaskPlanes = lsst.pex.config.ListField(
270270
dtype=str,
271-
default=("NO_DATA", "BAD", "SAT", "EDGE", "FAKE"),
272-
doc="Mask planes to exclude when selecting sources for PSF matching.",
273-
deprecated="No longer used. Will be removed after v30"
271+
default=("NO_DATA", "BAD", "SAT", "EDGE", "FAKE", "HIGH_VARIANCE"),
272+
doc="Template mask planes to exclude when selecting sources for PSF matching.",
274273
)
275274
badMaskPlanes = lsst.pex.config.ListField(
276275
dtype=str,
@@ -279,7 +278,7 @@ class AlardLuptonSubtractBaseConfig(lsst.pex.config.Config):
279278
)
280279
preserveTemplateMask = lsst.pex.config.ListField(
281280
dtype=str,
282-
default=("NO_DATA", "BAD",),
281+
default=("NO_DATA", "BAD", "HIGH_VARIANCE"),
283282
doc="Mask planes from the template to propagate to the image difference."
284283
)
285284
renameTemplateMask = lsst.pex.config.ListField(
@@ -443,7 +442,7 @@ def run(self, template, science, sources, visitSummary=None):
443442

444443
convolveTemplate = self.chooseConvolutionMethod(template, science)
445444

446-
selectSources = self._sourceSelector(sources, science.getBBox())
445+
selectSources = self._sourceSelector(sources, science.getBBox(), template.mask)
447446

448447
kernelResult = self.runMakeKernel(template, science, selectSources,
449448
convolveTemplate=convolveTemplate)
@@ -627,7 +626,7 @@ def runKernelSourceDetection(self, template, science):
627626
scienceFwhmPix=self.sciencePsfSize)
628627

629628
# return sources
630-
return self._sourceSelector(sources, science.getBBox(), fallback=True)
629+
return self._sourceSelector(sources, science.getBBox(), template.mask, fallback=True)
631630

632631
def runConvolveTemplate(self, template, science, psfMatchingKernel, backgroundModel=None):
633632
"""Convolve the template image with a PSF-matching kernel and subtract
@@ -908,7 +907,7 @@ def _convolveExposure(self, exposure, kernel, convolutionControl,
908907
else:
909908
return convolvedExposure[bbox]
910909

911-
def _sourceSelector(self, sources, bbox, fallback=False):
910+
def _sourceSelector(self, sources, bbox, mask, fallback=False):
912911
"""Select sources from a catalog that meet the selection criteria.
913912
The selection criteria include any configured parameters of the
914913
`sourceSelector` subtask, as well as distance from the edge if
@@ -920,6 +919,13 @@ def _sourceSelector(self, sources, bbox, fallback=False):
920919
Input source catalog to select sources from.
921920
bbox : `lsst.geom.Box2I`
922921
Bounding box of the science image.
922+
mask : `lsst.afw.image.Mask`
923+
The mask plane of the template to use to reject kernel candidates.
924+
fallback : `bool`, optional
925+
Switch indicating the source selector is being called after
926+
running the fallback source detection subtask, which does not run a
927+
full set of measurement plugins and can't use the same settings for
928+
the source selector.
923929
924930
Returns
925931
-------
@@ -929,6 +935,9 @@ def _sourceSelector(self, sources, bbox, fallback=False):
929935
930936
Raises
931937
------
938+
InsufficientKernelSourcesError
939+
An AlgorithmError that is raised if there are not enough PSF
940+
candidates to construct the PSF matching kernel.
932941
RuntimeError
933942
If there are too few sources to compute the PSF matching kernel
934943
remaining after source selection.
@@ -945,6 +954,9 @@ def _sourceSelector(self, sources, bbox, fallback=False):
945954
np.count_nonzero(~bboxSelected), rejectRadius)
946955
selected &= bboxSelected
947956
selectSources = sources[selected].copy(deep=True)
957+
# Optionally remove sources that land on masked pixels
958+
maskSelected = checkMask(mask, selectSources, self.config.excludeMaskPlanes, checkAdjacent=True)
959+
selectSources = selectSources[maskSelected].copy(deep=True)
948960
# Trim selectSources if they exceed ``maxKernelSources``.
949961
# Keep the highest signal-to-noise sources of those selected.
950962
if (len(selectSources) > self.config.maxKernelSources) & (self.config.maxKernelSources > 0):
@@ -1211,7 +1223,7 @@ def run(self, template, science, sources, visitSummary=None):
12111223
interpolateBadMaskPlanes=True)
12121224
self.metadata["convolvedExposure"] = "Preconvolution"
12131225
try:
1214-
selectSources = self._sourceSelector(sources, science.getBBox())
1226+
selectSources = self._sourceSelector(sources, science.getBBox(), template.mask)
12151227
subtractResults = self.runPreconvolve(template, science, matchedScience,
12161228
selectSources, scienceKernel)
12171229

python/lsst/ip/diffim/utils.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424

2525
__all__ = ["evaluateMeanPsfFwhm", "getPsfFwhm", "getKernelCenterDisplacement",
26-
"computeDifferenceImageMetrics",
26+
"computeDifferenceImageMetrics", "checkMask"
2727
]
2828

2929
import itertools
@@ -454,3 +454,44 @@ def populate_sattle_visit_cache(visit_info, historical=False):
454454
"historical": historical})
455455

456456
r.raise_for_status()
457+
458+
459+
def checkMask(mask, sources, excludeMaskPlanes, checkAdjacent=True):
460+
"""Exclude sources that are located on or adjacent to masked pixels.
461+
462+
Parameters
463+
----------
464+
mask : `lsst.afw.image.Mask`
465+
The image mask plane to use to reject sources
466+
based on the location of their centroid on the ccd.
467+
sources : `lsst.afw.table.SourceCatalog`
468+
The source catalog to evaluate.
469+
excludeMaskPlanes : `list` of `str`
470+
List of the names of the mask planes to exclude.
471+
472+
Returns
473+
-------
474+
flags : `numpy.ndarray` of `bool`
475+
Array indicating whether each source in the catalog should be
476+
kept (True) or rejected (False) based on the value of the
477+
mask plane at its location.
478+
"""
479+
setExcludeMaskPlanes = [
480+
maskPlane for maskPlane in excludeMaskPlanes if maskPlane in mask.getMaskPlaneDict()
481+
]
482+
483+
excludePixelMask = mask.getPlaneBitMask(setExcludeMaskPlanes)
484+
485+
xv = (np.rint(sources.getX() - mask.getX0())).astype(int)
486+
yv = (np.rint(sources.getY() - mask.getY0())).astype(int)
487+
488+
flags = np.ones(len(sources), dtype=bool)
489+
if checkAdjacent:
490+
pixRange = (0, -1, 1)
491+
else:
492+
pixRange = (0,)
493+
for j in pixRange:
494+
for i in pixRange:
495+
mv = mask.array[yv + j, xv + i]
496+
flags *= np.bitwise_and(mv, excludePixelMask) == 0
497+
return flags

tests/test_subtractTask.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,9 @@ def test_incomplete_template_coverage(self):
117117
border = 20
118118
xSize = 400
119119
ySize = 400
120-
science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6, nSrc=50,
120+
science, sources = makeTestImage(psfSize=3.0, noiseLevel=noiseLevel, noiseSeed=6, nSrc=100,
121121
xSize=xSize, ySize=ySize)
122-
template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7, nSrc=50,
122+
template, _ = makeTestImage(psfSize=2.0, noiseLevel=noiseLevel, noiseSeed=7, nSrc=100,
123123
templateBorderSize=border, doApplyCalibration=True,
124124
xSize=xSize, ySize=ySize)
125125

@@ -485,7 +485,7 @@ def _run_and_check_sources(sourcesIn, maxKernelSources=1000, minKernelSources=3,
485485
signalToNoise = sources.getPsfInstFlux()/sources.getPsfInstFluxErr()
486486
signalToNoise = signalToNoise[~sources[badSourceFlag]]
487487
signalToNoise.sort()
488-
selectSources = task._sourceSelector(sources, science.getBBox())
488+
selectSources = task._sourceSelector(sources, science.getBBox(), template.mask)
489489
self.assertEqual(nGoodSources, len(selectSources))
490490
signalToNoiseOut = selectSources.getPsfInstFlux()/selectSources.getPsfInstFluxErr()
491491
signalToNoiseOut.sort()

0 commit comments

Comments
 (0)