Skip to content

Commit f369c22

Browse files
authored
Merge pull request #426 from lsst/tickets/DM-52108
DM-52108: Add simplified image differencing task
2 parents 695406a + 830e0b4 commit f369c22

3 files changed

Lines changed: 285 additions & 18 deletions

File tree

python/lsst/ip/diffim/makeKernel.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ def setDefaults(self):
8080
# Minimal set of measurments for star selection
8181
self.selectMeasurement.algorithms.names.clear()
8282
self.selectMeasurement.algorithms.names = ('base_SdssCentroid', 'base_PsfFlux', 'base_PixelFlags',
83-
'base_SdssShape', 'base_GaussianFlux', 'base_SkyCoord')
83+
'base_SdssShape', 'base_GaussianFlux', 'base_SkyCoord',
84+
'base_ClassificationSizeExtendedness')
8485
self.selectMeasurement.slots.modelFlux = None
8586
self.selectMeasurement.slots.apFlux = None
8687
self.selectMeasurement.slots.calibFlux = None

python/lsst/ip/diffim/subtractImages.py

Lines changed: 227 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737

3838
__all__ = ["AlardLuptonSubtractConfig", "AlardLuptonSubtractTask",
3939
"AlardLuptonPreconvolveSubtractConfig", "AlardLuptonPreconvolveSubtractTask",
40+
"SimplifiedSubtractConfig", "SimplifiedSubtractTask",
4041
"InsufficientKernelSourcesError"]
4142

4243
_dimensions = ("instrument", "visit", "detector")
@@ -154,6 +155,24 @@ class AlardLuptonSubtractConnections(SubtractInputConnections, SubtractImageOutp
154155
pass
155156

156157

158+
class SimplifiedSubtractConnections(SubtractInputConnections, SubtractImageOutputConnections):
159+
inputPsfMatchingKernel = connectionTypes.Input(
160+
doc="Kernel used to PSF match the science and template images.",
161+
dimensions=("instrument", "visit", "detector"),
162+
storageClass="MatchingKernel",
163+
name="{fakesType}{coaddName}Diff_psfMatchKernel",
164+
)
165+
166+
def __init__(self, *, config=None):
167+
super().__init__(config=config)
168+
del self.sources
169+
if config.useExistingKernel:
170+
del self.psfMatchingKernel
171+
del self.kernelSources
172+
else:
173+
del self.inputPsfMatchingKernel
174+
175+
157176
class AlardLuptonSubtractBaseConfig(lsst.pex.config.Config):
158177
makeKernel = lsst.pex.config.ConfigurableField(
159178
target=MakeKernelTask,
@@ -209,6 +228,14 @@ class AlardLuptonSubtractBaseConfig(lsst.pex.config.Config):
209228
target=ScienceSourceSelectorTask,
210229
doc="Task to select sources to be used for PSF matching.",
211230
)
231+
fallbackSourceSelector = lsst.pex.config.ConfigurableField(
232+
target=ScienceSourceSelectorTask,
233+
doc="Task to select sources to be used for PSF matching."
234+
"Used only if the kernel calculation fails and"
235+
"`allowKernelSourceDetection` is set. The fallback source detection"
236+
" will not include all of the same plugins as the original source "
237+
" detection, so not all of the same flags can be used.",
238+
)
212239
detectionThreshold = lsst.pex.config.Field(
213240
dtype=float,
214241
default=10,
@@ -282,6 +309,14 @@ def setDefaults(self):
282309
self.sourceSelector.doSignalToNoise = True # apply signal to noise filter
283310
self.sourceSelector.signalToNoise.minimum = 10
284311
self.sourceSelector.signalToNoise.maximum = 500
312+
self.fallbackSourceSelector.doSkySources = False # Do not include sky sources
313+
self.fallbackSourceSelector.doSignalToNoise = True # apply signal to noise filter
314+
self.fallbackSourceSelector.signalToNoise.minimum = 10
315+
# The following two configs should not be necessary to be turned on for
316+
# PSF-matching, and the fallback kernel source selection will fail if
317+
# they are set since it does not run deblending.
318+
self.fallbackSourceSelector.doIsolated = False # Do not apply isolated star selection
319+
self.fallbackSourceSelector.doRequirePrimary = False # Do not apply primary flag selection
285320

286321

287322
class AlardLuptonSubtractConfig(AlardLuptonSubtractBaseConfig, lsst.pipe.base.PipelineTaskConfig,
@@ -308,6 +343,7 @@ def __init__(self, **kwargs):
308343
self.makeSubtask("decorrelate")
309344
self.makeSubtask("makeKernel")
310345
self.makeSubtask("sourceSelector")
346+
self.makeSubtask("fallbackSourceSelector")
311347
if self.config.doScaleVariance:
312348
self.makeSubtask("scaleVariance")
313349

@@ -541,17 +577,7 @@ def runMakeKernel(self, template, science, sources, convolveTemplate=True):
541577
if self.config.allowKernelSourceDetection and convolveTemplate:
542578
self.log.warning("Error encountered trying to construct the matching kernel"
543579
f" Running source detection and retrying. {e}")
544-
kernelSize = self.makeKernel.makeKernelBasisList(
545-
referenceFwhmPix, targetFwhmPix)[0].getWidth()
546-
sigmaToFwhm = 2*np.log(2*np.sqrt(2))
547-
candidateList = self.makeKernel.makeCandidateList(reference, target, kernelSize,
548-
candidateList=None,
549-
sigma=targetFwhmPix/sigmaToFwhm)
550-
kernelSources = self.makeKernel.selectKernelSources(reference, target,
551-
candidateList=candidateList,
552-
preconvolved=False,
553-
templateFwhmPix=referenceFwhmPix,
554-
scienceFwhmPix=targetFwhmPix)
580+
kernelSources = self.runKernelSourceDetection(template, science)
555581
kernelResult = self.makeKernel.run(reference, target, kernelSources,
556582
preconvolved=False,
557583
templateFwhmPix=referenceFwhmPix,
@@ -571,6 +597,38 @@ def runMakeKernel(self, template, science, sources, convolveTemplate=True):
571597
psfMatchingKernel=kernelResult.psfMatchingKernel,
572598
kernelSources=kernelSources)
573599

600+
def runKernelSourceDetection(self, template, science):
601+
"""Run detection on the science image and use the template mask plane
602+
to reject candidate sources.
603+
604+
Parameters
605+
----------
606+
template : `lsst.afw.image.ExposureF`
607+
Template exposure, warped to match the science exposure.
608+
science : `lsst.afw.image.ExposureF`
609+
Science exposure to subtract from the template.
610+
611+
Returns
612+
-------
613+
kernelSources : `lsst.afw.table.SourceCatalog`
614+
Sources from the input catalog to use to construct the
615+
PSF-matching kernel.
616+
"""
617+
kernelSize = self.makeKernel.makeKernelBasisList(
618+
self.templatePsfSize, self.sciencePsfSize)[0].getWidth()
619+
sigmaToFwhm = 2*np.log(2*np.sqrt(2))
620+
candidateList = self.makeKernel.makeCandidateList(template, science, kernelSize,
621+
candidateList=None,
622+
sigma=self.sciencePsfSize/sigmaToFwhm)
623+
sources = self.makeKernel.selectKernelSources(template, science,
624+
candidateList=candidateList,
625+
preconvolved=False,
626+
templateFwhmPix=self.templatePsfSize,
627+
scienceFwhmPix=self.sciencePsfSize)
628+
629+
# return sources
630+
return self._sourceSelector(sources, science.getBBox(), fallback=True)
631+
574632
def runConvolveTemplate(self, template, science, psfMatchingKernel, backgroundModel=None):
575633
"""Convolve the template image with a PSF-matching kernel and subtract
576634
from the science image.
@@ -850,7 +908,7 @@ def _convolveExposure(self, exposure, kernel, convolutionControl,
850908
else:
851909
return convolvedExposure[bbox]
852910

853-
def _sourceSelector(self, sources, bbox):
911+
def _sourceSelector(self, sources, bbox, fallback=False):
854912
"""Select sources from a catalog that meet the selection criteria.
855913
The selection criteria include any configured parameters of the
856914
`sourceSelector` subtask, as well as distance from the edge if
@@ -875,8 +933,10 @@ def _sourceSelector(self, sources, bbox):
875933
If there are too few sources to compute the PSF matching kernel
876934
remaining after source selection.
877935
"""
878-
879-
selected = self.sourceSelector.selectSources(sources).selected
936+
if fallback:
937+
selected = self.fallbackSourceSelector.selectSources(sources).selected
938+
else:
939+
selected = self.sourceSelector.selectSources(sources).selected
880940
if self.config.restrictKernelEdgeSources:
881941
rejectRadius = 2*self.config.makeKernel.kernel.active.kernelSize
882942
bbox.grow(-rejectRadius)
@@ -1356,6 +1416,159 @@ def _shapeTest(exp1, exp2, fwhmExposureBuffer, fwhmExposureGrid):
13561416
return xTest | yTest
13571417

13581418

1419+
class SimplifiedSubtractConfig(AlardLuptonSubtractBaseConfig, lsst.pipe.base.PipelineTaskConfig,
1420+
pipelineConnections=SimplifiedSubtractConnections):
1421+
mode = lsst.pex.config.ChoiceField(
1422+
dtype=str,
1423+
default="convolveTemplate",
1424+
allowed={"auto": "Choose which image to convolve at runtime.",
1425+
"convolveScience": "Only convolve the science image.",
1426+
"convolveTemplate": "Only convolve the template image."},
1427+
doc="Choose which image to convolve at runtime, or require that a specific image is convolved."
1428+
)
1429+
useExistingKernel = lsst.pex.config.Field(
1430+
dtype=bool,
1431+
default=True,
1432+
doc="Use a pre-existing PSF matching kernel?"
1433+
"If False, source detection and measurement will be run."
1434+
)
1435+
1436+
1437+
class SimplifiedSubtractTask(AlardLuptonSubtractTask):
1438+
"""Compute the image difference of a science and template image using
1439+
the Alard & Lupton (1998) algorithm.
1440+
"""
1441+
ConfigClass = SimplifiedSubtractConfig
1442+
_DefaultName = "simplifiedSubtract"
1443+
1444+
@timeMethod
1445+
def run(self, template, science, visitSummary=None, inputPsfMatchingKernel=None):
1446+
"""PSF match, subtract, and decorrelate two images.
1447+
1448+
Parameters
1449+
----------
1450+
template : `lsst.afw.image.ExposureF`
1451+
Template exposure, warped to match the science exposure.
1452+
science : `lsst.afw.image.ExposureF`
1453+
Science exposure to subtract from the template.
1454+
visitSummary : `lsst.afw.table.ExposureCatalog`, optional
1455+
Exposure catalog with external calibrations to be applied. Catalog
1456+
uses the detector id for the catalog id, sorted on id for fast
1457+
lookup.
1458+
1459+
Returns
1460+
-------
1461+
results : `lsst.pipe.base.Struct`
1462+
``difference`` : `lsst.afw.image.ExposureF`
1463+
Result of subtracting template and science.
1464+
``matchedTemplate`` : `lsst.afw.image.ExposureF`
1465+
Warped and PSF-matched template exposure.
1466+
``backgroundModel`` : `lsst.afw.math.Function2D`
1467+
Background model that was fit while solving for the
1468+
PSF-matching kernel
1469+
``psfMatchingKernel`` : `lsst.afw.math.Kernel`
1470+
Kernel used to PSF-match the convolved image.
1471+
``kernelSources` : `lsst.afw.table.SourceCatalog`
1472+
Sources detected on the science image that were used to
1473+
construct the PSF-matching kernel.
1474+
1475+
Raises
1476+
------
1477+
RuntimeError
1478+
If an unsupported convolution mode is supplied.
1479+
RuntimeError
1480+
If there are too few sources to calculate the PSF matching kernel.
1481+
lsst.pipe.base.NoWorkFound
1482+
Raised if fraction of good pixels, defined as not having NO_DATA
1483+
set, is less then the configured requiredTemplateFraction
1484+
"""
1485+
self._prepareInputs(template, science, visitSummary=visitSummary)
1486+
1487+
convolveTemplate = self.chooseConvolutionMethod(template, science)
1488+
1489+
if self.config.useExistingKernel:
1490+
psfMatchingKernel = inputPsfMatchingKernel
1491+
backgroundModel = None
1492+
kernelSources = None
1493+
else:
1494+
kernelResult = self.runMakeKernel(template, science, convolveTemplate=convolveTemplate)
1495+
psfMatchingKernel = kernelResult.psfMatchingKernel
1496+
kernelSources = kernelResult.kernelSources
1497+
if self.config.doSubtractBackground:
1498+
backgroundModel = kernelResult.backgroundModel
1499+
else:
1500+
backgroundModel = None
1501+
if convolveTemplate:
1502+
subtractResults = self.runConvolveTemplate(template, science, psfMatchingKernel,
1503+
backgroundModel=backgroundModel)
1504+
else:
1505+
subtractResults = self.runConvolveScience(template, science, psfMatchingKernel,
1506+
backgroundModel=backgroundModel)
1507+
if kernelSources is not None:
1508+
subtractResults.kernelSources = kernelSources
1509+
return subtractResults
1510+
1511+
def runMakeKernel(self, template, science, convolveTemplate=True):
1512+
"""Construct the PSF-matching kernel.
1513+
1514+
Parameters
1515+
----------
1516+
template : `lsst.afw.image.ExposureF`
1517+
Template exposure, warped to match the science exposure.
1518+
science : `lsst.afw.image.ExposureF`
1519+
Science exposure to subtract from the template.
1520+
sources : `lsst.afw.table.SourceCatalog`
1521+
Identified sources on the science exposure. This catalog is used to
1522+
select sources in order to perform the AL PSF matching on stamp
1523+
images around them.
1524+
convolveTemplate : `bool`, optional
1525+
Construct the matching kernel to convolve the template?
1526+
1527+
Returns
1528+
-------
1529+
results : `lsst.pipe.base.Struct`
1530+
``backgroundModel`` : `lsst.afw.math.Function2D`
1531+
Background model that was fit while solving for the
1532+
PSF-matching kernel
1533+
``psfMatchingKernel`` : `lsst.afw.math.Kernel`
1534+
Kernel used to PSF-match the convolved image.
1535+
``kernelSources` : `lsst.afw.table.SourceCatalog`
1536+
Sources from the input catalog that were used to construct the
1537+
PSF-matching kernel.
1538+
"""
1539+
if convolveTemplate:
1540+
reference = template
1541+
target = science
1542+
referenceFwhmPix = self.templatePsfSize
1543+
targetFwhmPix = self.sciencePsfSize
1544+
else:
1545+
reference = science
1546+
target = template
1547+
referenceFwhmPix = self.sciencePsfSize
1548+
targetFwhmPix = self.templatePsfSize
1549+
try:
1550+
# The try..except block catches any error, and raises
1551+
# NoWorkFound if the template coverage is insufficient. Otherwise,
1552+
# the original error is raised.
1553+
kernelSources = self.runKernelSourceDetection(template, science)
1554+
kernelResult = self.makeKernel.run(reference, target, kernelSources,
1555+
preconvolved=False,
1556+
templateFwhmPix=referenceFwhmPix,
1557+
scienceFwhmPix=targetFwhmPix)
1558+
except (RuntimeError, lsst.pex.exceptions.Exception) as e:
1559+
self.log.warning("Failed to match template. Checking coverage")
1560+
# Raise NoWorkFound if template fraction is insufficient
1561+
checkTemplateIsSufficient(template[science.getBBox()], science, self.log,
1562+
self.config.minTemplateFractionForExpectedSuccess,
1563+
exceptionMessage="Template coverage lower than expected to succeed."
1564+
f" Failure is tolerable: {e}")
1565+
# checkTemplateIsSufficient did not raise NoWorkFound, so raise original exception
1566+
raise e
1567+
return lsst.pipe.base.Struct(backgroundModel=kernelResult.backgroundModel,
1568+
psfMatchingKernel=kernelResult.psfMatchingKernel,
1569+
kernelSources=kernelSources)
1570+
1571+
13591572
def _interpolateImage(maskedImage, badMaskPlanes, fallbackValue=None):
13601573
"""Replace masked image pixels with interpolated values.
13611574

0 commit comments

Comments
 (0)