22from astropy
import units
as u
29from lsst.ip.diffim.utils
import evaluateMeanPsfFwhm, getPsfFwhm
35from .
import MakeKernelTask, DecorrelateALKernelTask
36from lsst.utils.timer
import timeMethod
38__all__ = [
"AlardLuptonSubtractConfig",
"AlardLuptonSubtractTask",
39 "AlardLuptonPreconvolveSubtractConfig",
"AlardLuptonPreconvolveSubtractTask"]
41_dimensions = (
"instrument",
"visit",
"detector")
42_defaultTemplates = {
"coaddName":
"deep",
"fakesType":
""}
46 dimensions=_dimensions,
47 defaultTemplates=_defaultTemplates):
48 template = connectionTypes.Input(
49 doc=
"Input warped template to subtract.",
50 dimensions=(
"instrument",
"visit",
"detector"),
51 storageClass=
"ExposureF",
52 name=
"{fakesType}{coaddName}Diff_templateExp"
54 science = connectionTypes.Input(
55 doc=
"Input science exposure to subtract from.",
56 dimensions=(
"instrument",
"visit",
"detector"),
57 storageClass=
"ExposureF",
58 name=
"{fakesType}calexp"
60 sources = connectionTypes.Input(
61 doc=
"Sources measured on the science exposure; "
62 "used to select sources for making the matching kernel.",
63 dimensions=(
"instrument",
"visit",
"detector"),
64 storageClass=
"SourceCatalog",
67 visitSummary = connectionTypes.Input(
68 doc=(
"Per-visit catalog with final calibration objects. "
69 "These catalogs use the detector id for the catalog id, "
70 "sorted on id for fast lookup."),
71 dimensions=(
"instrument",
"visit"),
72 storageClass=
"ExposureCatalog",
73 name=
"finalVisitSummary",
78 if not config.doApplyExternalCalibrations:
83 dimensions=_dimensions,
84 defaultTemplates=_defaultTemplates):
85 difference = connectionTypes.Output(
86 doc=
"Result of subtracting convolved template from science image.",
87 dimensions=(
"instrument",
"visit",
"detector"),
88 storageClass=
"ExposureF",
89 name=
"{fakesType}{coaddName}Diff_differenceTempExp",
91 matchedTemplate = connectionTypes.Output(
92 doc=
"Warped and PSF-matched template used to create `subtractedExposure`.",
93 dimensions=(
"instrument",
"visit",
"detector"),
94 storageClass=
"ExposureF",
95 name=
"{fakesType}{coaddName}Diff_matchedExp",
97 psfMatchingKernel = connectionTypes.Output(
98 doc=
"Kernel used to PSF match the science and template images.",
99 dimensions=(
"instrument",
"visit",
"detector"),
100 storageClass=
"MatchingKernel",
101 name=
"{fakesType}{coaddName}Diff_psfMatchKernel",
103 kernelSources = connectionTypes.Output(
104 doc=
"Final selection of sources used for psf matching.",
105 dimensions=(
"instrument",
"visit",
"detector"),
106 storageClass=
"SourceCatalog",
107 name=
"{fakesType}{coaddName}Diff_psfMatchSources"
112 dimensions=_dimensions,
113 defaultTemplates=_defaultTemplates):
114 scoreExposure = connectionTypes.Output(
115 doc=
"The maximum likelihood image, used for the detection of diaSources.",
116 dimensions=(
"instrument",
"visit",
"detector"),
117 storageClass=
"ExposureF",
118 name=
"{fakesType}{coaddName}Diff_scoreExp",
120 psfMatchingKernel = connectionTypes.Output(
121 doc=
"Kernel used to PSF match the science and template images.",
122 dimensions=(
"instrument",
"visit",
"detector"),
123 storageClass=
"MatchingKernel",
124 name=
"{fakesType}{coaddName}Diff_psfScoreMatchKernel",
126 kernelSources = connectionTypes.Output(
127 doc=
"Final selection of sources used for psf matching.",
128 dimensions=(
"instrument",
"visit",
"detector"),
129 storageClass=
"SourceCatalog",
130 name=
"{fakesType}{coaddName}Diff_psfScoreMatchSources"
140 target=MakeKernelTask,
141 doc=
"Task to construct a matching kernel for convolution.",
146 doc=
"Perform diffim decorrelation to undo pixel correlation due to A&L "
147 "kernel convolution? If True, also update the diffim PSF."
150 target=DecorrelateALKernelTask,
151 doc=
"Task to decorrelate the image difference.",
156 doc=
"Raise NoWorkFound and do not attempt image subtraction if template covers less than this "
157 " fraction of pixels. Setting to 0 will always attempt image subtraction."
162 doc=
"Raise NoWorkFound if PSF-matching fails and template covers less than this fraction of pixels."
163 " If the fraction of pixels covered by the template is less than this value (and greater than"
164 " requiredTemplateFraction) this task is attempted but failure is anticipated and tolerated."
169 doc=
"Scale variance of the image difference?"
172 target=ScaleVarianceTask,
173 doc=
"Subtask to rescale the variance of the template to the statistically expected level."
176 doc=
"Subtract the background fit when solving the kernel?",
182 "Replace science Exposure's calibration objects with those"
183 " in visitSummary. Ignored if `doApplyFinalizedPsf is True."
189 target=ScienceSourceSelectorTask,
190 doc=
"Task to select sources to be used for PSF matching.",
195 doc=
"Minimum signal to noise ratio of detected sources "
196 "to use for calculating the PSF matching kernel."
201 doc=
"Maximum signal to noise ratio of detected sources "
202 "to use for calculating the PSF matching kernel."
207 doc=
"Maximum number of sources to use for calculating the PSF matching kernel."
208 "Set to -1 to disable."
213 doc=
"Minimum number of sources needed for calculating the PSF matching kernel."
217 default=(
"NO_DATA",
"BAD",
"SAT",
"EDGE",
"FAKE"),
218 doc=
"Mask planes to exclude when selecting sources for PSF matching."
222 default=(
"NO_DATA",
"BAD",
"SAT",
"EDGE"),
223 doc=
"Mask planes to interpolate over."
227 default=(
"NO_DATA",
"BAD",),
228 doc=
"Mask planes from the template to propagate to the image difference."
232 default=(
"SAT",
"INJECTED",
"INJECTED_CORE",),
233 doc=
"Mask planes from the template to propagate to the image difference"
234 "with '_TEMPLATE' appended to the name."
239 doc=
"Re-run source detection for kernel candidates if an error is"
240 " encountered while calculating the matching kernel."
246 self.
makeKernel.kernel.active.spatialKernelOrder = 1
247 self.
makeKernel.kernel.active.spatialBgOrder = 2
258 pipelineConnections=AlardLuptonSubtractConnections):
261 default=
"convolveTemplate",
262 allowed={
"auto":
"Choose which image to convolve at runtime.",
263 "convolveScience":
"Only convolve the science image.",
264 "convolveTemplate":
"Only convolve the template image."},
265 doc=
"Choose which image to convolve at runtime, or require that a specific image is convolved."
270 """Compute the image difference of a science and template image using
271 the Alard & Lupton (1998) algorithm.
273 ConfigClass = AlardLuptonSubtractConfig
274 _DefaultName =
"alardLuptonSubtract"
278 self.makeSubtask(
"decorrelate")
279 self.makeSubtask(
"makeKernel")
280 self.makeSubtask(
"sourceSelector")
281 if self.config.doScaleVariance:
282 self.makeSubtask(
"scaleVariance")
291 """Replace calibrations (psf, and ApCorrMap) on this exposure with
296 exposure : `lsst.afw.image.exposure.Exposure`
297 Input exposure to adjust calibrations.
298 visitSummary : `lsst.afw.table.ExposureCatalog`
299 Exposure catalog with external calibrations to be applied. Catalog
300 uses the detector id for the catalog id, sorted on id for fast
305 exposure : `lsst.afw.image.exposure.Exposure`
306 Exposure with adjusted calibrations.
308 detectorId = exposure.info.getDetector().getId()
310 row = visitSummary.find(detectorId)
312 self.
log.warning(
"Detector id %s not found in external calibrations catalog; "
313 "Using original calibrations.", detectorId)
316 apCorrMap = row.getApCorrMap()
318 self.
log.warning(
"Detector id %s has None for psf in "
319 "external calibrations catalog; Using original psf and aperture correction.",
321 elif apCorrMap
is None:
322 self.
log.warning(
"Detector id %s has None for apCorrMap in "
323 "external calibrations catalog; Using original psf and aperture correction.",
327 exposure.info.setApCorrMap(apCorrMap)
332 def run(self, template, science, sources, visitSummary=None):
333 """PSF match, subtract, and decorrelate two images.
337 template : `lsst.afw.image.ExposureF`
338 Template exposure, warped to match the science exposure.
339 science : `lsst.afw.image.ExposureF`
340 Science exposure to subtract from the template.
341 sources : `lsst.afw.table.SourceCatalog`
342 Identified sources on the science exposure. This catalog is used to
343 select sources in order to perform the AL PSF matching on stamp
345 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
346 Exposure catalog with external calibrations to be applied. Catalog
347 uses the detector id for the catalog id, sorted on id for fast
352 results : `lsst.pipe.base.Struct`
353 ``difference`` : `lsst.afw.image.ExposureF`
354 Result of subtracting template and science.
355 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
356 Warped and PSF-matched template exposure.
357 ``backgroundModel`` : `lsst.afw.math.Function2D`
358 Background model that was fit while solving for the
360 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
361 Kernel used to PSF-match the convolved image.
366 If an unsupported convolution mode is supplied.
368 If there are too few sources to calculate the PSF matching kernel.
369 lsst.pipe.base.NoWorkFound
370 Raised if fraction of good pixels, defined as not having NO_DATA
371 set, is less then the configured requiredTemplateFraction
377 fwhmExposureBuffer = self.config.makeKernel.fwhmExposureBuffer
378 fwhmExposureGrid = self.config.makeKernel.fwhmExposureGrid
395 self.
log.info(
"Unable to evaluate PSF at the average position. "
396 "Evaluting PSF on a grid of points."
399 fwhmExposureBuffer=fwhmExposureBuffer,
400 fwhmExposureGrid=fwhmExposureGrid
403 fwhmExposureBuffer=fwhmExposureBuffer,
404 fwhmExposureGrid=fwhmExposureGrid
413 fluxlim_science = (maglim_science*u.ABmag).to_value(u.nJy)
415 if np.isnan(maglim_template):
416 self.
log.info(
"Cannot evaluate template limiting mag; adopting science limiting mag for diffim")
417 maglim_diffim = maglim_science
419 fluxlim_template = (maglim_template*u.ABmag).to_value(u.nJy)
420 maglim_diffim = (np.sqrt(fluxlim_science**2 + fluxlim_template**2)*u.nJy).to(u.ABmag).value
421 self.metadata[
"scienceLimitingMagnitude"] = maglim_science
422 self.metadata[
"templateLimitingMagnitude"] = maglim_template
423 self.metadata[
"diffimLimitingMagnitude"] = maglim_diffim
425 if self.config.mode ==
"auto":
428 fwhmExposureBuffer=fwhmExposureBuffer,
429 fwhmExposureGrid=fwhmExposureGrid)
432 self.
log.info(
"Average template PSF size is greater, "
433 "but science PSF greater in one dimension: convolving template image.")
435 self.
log.info(
"Science PSF size is greater: convolving template image.")
437 self.
log.info(
"Template PSF size is greater: convolving science image.")
438 elif self.config.mode ==
"convolveTemplate":
439 self.
log.info(
"`convolveTemplate` is set: convolving template image.")
440 convolveTemplate =
True
441 elif self.config.mode ==
"convolveScience":
442 self.
log.info(
"`convolveScience` is set: convolving science image.")
443 convolveTemplate =
False
445 raise RuntimeError(
"Cannot handle AlardLuptonSubtract mode: %s", self.config.mode)
448 sourceMask = science.mask.clone()
449 sourceMask.array |= template[science.getBBox()].mask.array
452 self.metadata[
"convolvedExposure"] =
"Template"
455 self.metadata[
"convolvedExposure"] =
"Science"
459 self.
log.warning(
"Failed to match template. Checking coverage")
462 self.config.minTemplateFractionForExpectedSuccess,
463 exceptionMessage=
"Template coverage lower than expected to succeed."
464 f
" Failure is tolerable: {e}")
468 return subtractResults
471 """Convolve the template image with a PSF-matching kernel and subtract
472 from the science image.
476 template : `lsst.afw.image.ExposureF`
477 Template exposure, warped to match the science exposure.
478 science : `lsst.afw.image.ExposureF`
479 Science exposure to subtract from the template.
480 selectSources : `lsst.afw.table.SourceCatalog`
481 Identified sources on the science exposure. This catalog is used to
482 select sources in order to perform the AL PSF matching on stamp
487 results : `lsst.pipe.base.Struct`
489 ``difference`` : `lsst.afw.image.ExposureF`
490 Result of subtracting template and science.
491 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
492 Warped and PSF-matched template exposure.
493 ``backgroundModel`` : `lsst.afw.math.Function2D`
494 Background model that was fit while solving for the PSF-matching kernel
495 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
496 Kernel used to PSF-match the template to the science image.
499 kernelSources = self.makeKernel.selectKernelSources(template, science,
500 candidateList=selectSources,
502 kernelResult = self.makeKernel.run(template, science, kernelSources,
504 except Exception
as e:
505 if self.config.allowKernelSourceDetection:
506 self.
log.warning(
"Error encountered trying to construct the matching kernel"
507 f
" Running source detection and retrying. {e}")
508 kernelSize = self.makeKernel.makeKernelBasisList(
510 sigmaToFwhm = 2*np.log(2*np.sqrt(2))
511 candidateList = self.makeKernel.makeCandidateList(template, science, kernelSize,
514 kernelSources = self.makeKernel.selectKernelSources(template, science,
515 candidateList=candidateList,
517 kernelResult = self.makeKernel.run(template, science, kernelSources,
522 matchedTemplate = self.
_convolveExposure(template, kernelResult.psfMatchingKernel,
524 bbox=science.getBBox(),
526 photoCalib=science.photoCalib)
529 backgroundModel=(kernelResult.backgroundModel
530 if self.config.doSubtractBackground
else None))
531 correctedExposure = self.
finalize(template, science, difference,
532 kernelResult.psfMatchingKernel,
533 templateMatched=
True)
535 return lsst.pipe.base.Struct(difference=correctedExposure,
536 matchedTemplate=matchedTemplate,
537 matchedScience=science,
538 backgroundModel=kernelResult.backgroundModel,
539 psfMatchingKernel=kernelResult.psfMatchingKernel,
540 kernelSources=kernelSources)
543 """Convolve the science image with a PSF-matching kernel and subtract
548 template : `lsst.afw.image.ExposureF`
549 Template exposure, warped to match the science exposure.
550 science : `lsst.afw.image.ExposureF`
551 Science exposure to subtract from the template.
552 selectSources : `lsst.afw.table.SourceCatalog`
553 Identified sources on the science exposure. This catalog is used to
554 select sources in order to perform the AL PSF matching on stamp
559 results : `lsst.pipe.base.Struct`
561 ``difference`` : `lsst.afw.image.ExposureF`
562 Result of subtracting template and science.
563 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
564 Warped template exposure. Note that in this case, the template
565 is not PSF-matched to the science image.
566 ``backgroundModel`` : `lsst.afw.math.Function2D`
567 Background model that was fit while solving for the PSF-matching kernel
568 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
569 Kernel used to PSF-match the science image to the template.
571 bbox = science.getBBox()
572 kernelSources = self.makeKernel.selectKernelSources(science, template,
573 candidateList=selectSources,
575 kernelResult = self.makeKernel.run(science, template, kernelSources,
577 modelParams = kernelResult.backgroundModel.getParameters()
579 kernelResult.backgroundModel.setParameters([-p
for p
in modelParams])
581 kernelImage = lsst.afw.image.ImageD(kernelResult.psfMatchingKernel.getDimensions())
582 norm = kernelResult.psfMatchingKernel.computeImage(kernelImage, doNormalize=
False)
589 matchedScience.maskedImage /= norm
590 matchedTemplate = template.clone()[bbox]
591 matchedTemplate.maskedImage /= norm
592 matchedTemplate.setPhotoCalib(science.photoCalib)
595 backgroundModel=(kernelResult.backgroundModel
596 if self.config.doSubtractBackground
else None))
598 correctedExposure = self.
finalize(template, science, difference,
599 kernelResult.psfMatchingKernel,
600 templateMatched=
False)
602 return lsst.pipe.base.Struct(difference=correctedExposure,
603 matchedTemplate=matchedTemplate,
604 matchedScience=matchedScience,
605 backgroundModel=kernelResult.backgroundModel,
606 psfMatchingKernel=kernelResult.psfMatchingKernel,
607 kernelSources=kernelSources)
609 def finalize(self, template, science, difference, kernel,
610 templateMatched=True,
613 spatiallyVarying=False):
614 """Decorrelate the difference image to undo the noise correlations
615 caused by convolution.
619 template : `lsst.afw.image.ExposureF`
620 Template exposure, warped to match the science exposure.
621 science : `lsst.afw.image.ExposureF`
622 Science exposure to subtract from the template.
623 difference : `lsst.afw.image.ExposureF`
624 Result of subtracting template and science.
625 kernel : `lsst.afw.math.Kernel`
626 An (optionally spatially-varying) PSF matching kernel
627 templateMatched : `bool`, optional
628 Was the template PSF-matched to the science image?
629 preConvMode : `bool`, optional
630 Was the science image preconvolved with its own PSF
631 before PSF matching the template?
632 preConvKernel : `lsst.afw.detection.Psf`, optional
633 If not `None`, then the science image was pre-convolved with
634 (the reflection of) this kernel. Must be normalized to sum to 1.
635 spatiallyVarying : `bool`, optional
636 Compute the decorrelation kernel spatially varying across the image?
640 correctedExposure : `lsst.afw.image.ExposureF`
641 The decorrelated image difference.
643 if self.config.doDecorrelation:
644 self.
log.info(
"Decorrelating image difference.")
648 correctedExposure = self.decorrelate.run(science, template[science.getBBox()], difference, kernel,
649 templateMatched=templateMatched,
650 preConvMode=preConvMode,
651 preConvKernel=preConvKernel,
652 spatiallyVarying=spatiallyVarying).correctedExposure
654 self.
log.info(
"NOT decorrelating image difference.")
655 correctedExposure = difference
656 return correctedExposure
659 """Calculate an exposure's limiting magnitude.
661 This method uses the photometric zeropoint together with the
662 PSF size from the average position of the exposure.
666 exposure : `lsst.afw.image.Exposure`
667 The target exposure to calculate the limiting magnitude for.
668 nsigma : `float`, optional
669 The detection threshold in sigma.
670 fallbackPsfSize : `float`, optional
671 PSF FWHM to use in the event the exposure PSF cannot be retrieved.
675 maglim : `astropy.units.Quantity`
676 The limiting magnitude of the exposure, or np.nan.
679 psf = exposure.getPsf()
680 psf_shape = psf.computeShape(psf.getAveragePosition())
682 if fallbackPsfSize
is not None:
683 self.
log.info(
"Unable to evaluate PSF, using fallback FWHM %f", fallbackPsfSize)
684 psf_area = np.pi*(fallbackPsfSize/2)**2
685 zeropoint = exposure.getPhotoCalib().instFluxToMagnitude(1)
686 maglim = zeropoint - 2.5*np.log10(nsigma*np.sqrt(psf_area))
688 self.
log.info(
"Unable to evaluate PSF, setting maglim to nan")
692 psf_area = np.pi*np.sqrt(psf_shape.getIxx()*psf_shape.getIyy())
693 zeropoint = exposure.getPhotoCalib().instFluxToMagnitude(1)
694 maglim = zeropoint - 2.5*np.log10(nsigma*np.sqrt(psf_area))
700 """Check that the WCS of the two Exposures match, the template bbox
701 contains the science bbox, and that the bands match.
705 template : `lsst.afw.image.ExposureF`
706 Template exposure, warped to match the science exposure.
707 science : `lsst.afw.image.ExposureF`
708 Science exposure to subtract from the template.
713 Raised if the WCS of the template is not equal to the science WCS,
714 if the science image is not fully contained in the template
715 bounding box, or if the bands do not match.
717 assert template.wcs == science.wcs, \
718 "Template and science exposure WCS are not identical."
719 templateBBox = template.getBBox()
720 scienceBBox = science.getBBox()
721 assert science.filter.bandLabel == template.filter.bandLabel, \
722 "Science and template exposures have different bands: %s, %s" % \
723 (science.filter, template.filter)
725 assert templateBBox.contains(scienceBBox), \
726 "Template bbox does not contain all of the science image."
732 interpolateBadMaskPlanes=False,
734 """Convolve an exposure with the given kernel.
738 exposure : `lsst.afw.Exposure`
739 exposure to convolve.
740 kernel : `lsst.afw.math.LinearCombinationKernel`
741 PSF matching kernel computed in the ``makeKernel`` subtask.
742 convolutionControl : `lsst.afw.math.ConvolutionControl`
743 Configuration for convolve algorithm.
744 bbox : `lsst.geom.Box2I`, optional
745 Bounding box to trim the convolved exposure to.
746 psf : `lsst.afw.detection.Psf`, optional
747 Point spread function (PSF) to set for the convolved exposure.
748 photoCalib : `lsst.afw.image.PhotoCalib`, optional
749 Photometric calibration of the convolved exposure.
753 convolvedExp : `lsst.afw.Exposure`
756 convolvedExposure = exposure.clone()
758 convolvedExposure.setPsf(psf)
759 if photoCalib
is not None:
760 convolvedExposure.setPhotoCalib(photoCalib)
761 if interpolateBadMaskPlanes
and self.config.badMaskPlanes
is not None:
763 self.config.badMaskPlanes)
764 self.metadata[
"nInterpolated"] = nInterp
765 convolvedImage = lsst.afw.image.MaskedImageF(convolvedExposure.getBBox())
767 convolvedExposure.setMaskedImage(convolvedImage)
769 return convolvedExposure
771 return convolvedExposure[bbox]
774 """Select sources from a catalog that meet the selection criteria.
778 sources : `lsst.afw.table.SourceCatalog`
779 Input source catalog to select sources from.
780 mask : `lsst.afw.image.Mask`
781 The image mask plane to use to reject sources
782 based on their location on the ccd.
786 selectSources : `lsst.afw.table.SourceCatalog`
787 The input source catalog, with flagged and low signal-to-noise
793 If there are too few sources to compute the PSF matching kernel
794 remaining after source selection.
797 selected = self.sourceSelector.selectSources(sources).selected
798 nInitialSelected = np.count_nonzero(selected)
799 selected *= self.
_checkMask(mask, sources, self.config.excludeMaskPlanes)
800 nSelected = np.count_nonzero(selected)
801 self.
log.info(
"Rejecting %i candidate sources: an excluded template mask plane is set.",
802 nInitialSelected - nSelected)
803 selectSources = sources[selected].copy(deep=
True)
806 if (len(selectSources) > self.config.maxKernelSources) & (self.config.maxKernelSources > 0):
807 signalToNoise = selectSources.getPsfInstFlux()/selectSources.getPsfInstFluxErr()
808 indices = np.argsort(signalToNoise)
809 indices = indices[-self.config.maxKernelSources:]
810 selected = np.zeros(len(selectSources), dtype=bool)
811 selected[indices] =
True
812 selectSources = selectSources[selected].copy(deep=
True)
814 self.
log.info(
"%i/%i=%.1f%% of sources selected for PSF matching from the input catalog",
815 len(selectSources), len(sources), 100*len(selectSources)/len(sources))
816 if len(selectSources) < self.config.minKernelSources:
817 self.
log.error(
"Too few sources to calculate the PSF matching kernel: "
818 "%i selected but %i needed for the calculation.",
819 len(selectSources), self.config.minKernelSources)
820 if not self.config.allowKernelSourceDetection:
821 raise RuntimeError(
"Cannot compute PSF matching kernel: too few sources selected.")
822 self.metadata[
"nPsfSources"] = len(selectSources)
827 def _checkMask(mask, sources, excludeMaskPlanes, checkAdjacent=True):
828 """Exclude sources that are located on or adjacent to masked pixels.
832 mask : `lsst.afw.image.Mask`
833 The image mask plane to use to reject sources
834 based on the location of their centroid on the ccd.
835 sources : `lsst.afw.table.SourceCatalog`
836 The source catalog to evaluate.
837 excludeMaskPlanes : `list` of `str`
838 List of the names of the mask planes to exclude.
842 flags : `numpy.ndarray` of `bool`
843 Array indicating whether each source in the catalog should be
844 kept (True) or rejected (False) based on the value of the
845 mask plane at its location.
847 setExcludeMaskPlanes = [
848 maskPlane
for maskPlane
in excludeMaskPlanes
if maskPlane
in mask.getMaskPlaneDict()
851 excludePixelMask = mask.getPlaneBitMask(setExcludeMaskPlanes)
853 xv = (np.rint(sources.getX() - mask.getX0())).astype(int)
854 yv = (np.rint(sources.getY() - mask.getY0())).astype(int)
856 flags = np.ones(len(sources), dtype=bool)
858 pixRange = (0, -1, 1)
863 mv = mask.array[yv + j, xv + i]
864 flags *= np.bitwise_and(mv, excludePixelMask) == 0
868 """Perform preparatory calculations common to all Alard&Lupton Tasks.
872 template : `lsst.afw.image.ExposureF`
873 Template exposure, warped to match the science exposure. The
874 variance plane of the template image is modified in place.
875 science : `lsst.afw.image.ExposureF`
876 Science exposure to subtract from the template. The variance plane
877 of the science image is modified in place.
878 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
879 Exposure catalog with external calibrations to be applied. Catalog
880 uses the detector id for the catalog id, sorted on id for fast
884 if visitSummary
is not None:
887 template[science.getBBox()], self.
log,
888 requiredTemplateFraction=self.config.requiredTemplateFraction,
889 exceptionMessage=
"Not attempting subtraction. To force subtraction,"
890 " set config requiredTemplateFraction=0"
892 self.metadata[
"templateCoveragePercent"] = 100*templateCoverageFraction
894 if self.config.doScaleVariance:
898 templateVarFactor = self.scaleVariance.run(template.maskedImage)
899 sciVarFactor = self.scaleVariance.run(science.maskedImage)
900 self.
log.info(
"Template variance scaling factor: %.2f", templateVarFactor)
901 self.metadata[
"scaleTemplateVarianceFactor"] = templateVarFactor
902 self.
log.info(
"Science variance scaling factor: %.2f", sciVarFactor)
903 self.metadata[
"scaleScienceVarianceFactor"] = sciVarFactor
910 """Update the science and template mask planes before differencing.
914 template : `lsst.afw.image.Exposure`
915 Template exposure, warped to match the science exposure.
916 The template mask planes will be erased, except for a few specified
918 science : `lsst.afw.image.Exposure`
919 Science exposure to subtract from the template.
920 The DETECTED and DETECTED_NEGATIVE mask planes of the science image
923 self.
_clearMask(science.mask, clearMaskPlanes=[
"DETECTED",
"DETECTED_NEGATIVE"])
930 clearMaskPlanes = [mp
for mp
in template.mask.getMaskPlaneDict().keys()
931 if mp
not in self.config.preserveTemplateMask]
932 renameMaskPlanes = [mp
for mp
in self.config.renameTemplateMask
933 if mp
in template.mask.getMaskPlaneDict().keys()]
938 if "FAKE" in science.mask.getMaskPlaneDict().keys():
939 self.
log.info(
"Adding injected mask plane to science image")
941 if "FAKE" in template.mask.getMaskPlaneDict().keys():
942 self.
log.info(
"Adding injected mask plane to template image")
944 if "INJECTED" in renameMaskPlanes:
945 renameMaskPlanes.remove(
"INJECTED")
946 if "INJECTED_TEMPLATE" in clearMaskPlanes:
947 clearMaskPlanes.remove(
"INJECTED_TEMPLATE")
949 for maskPlane
in renameMaskPlanes:
951 self.
_clearMask(template.mask, clearMaskPlanes=clearMaskPlanes)
955 """Rename a mask plane by adding the new name and copying the data.
959 mask : `lsst.afw.image.Mask`
960 The mask image to update in place.
962 The name of the existing mask plane to copy.
964 The new name of the mask plane that will be added.
965 If the mask plane already exists, it will be updated in place.
967 mask.addMaskPlane(newMaskPlane)
968 originBitMask = mask.getPlaneBitMask(maskPlane)
969 destinationBitMask = mask.getPlaneBitMask(newMaskPlane)
970 mask.array |= ((mask.array & originBitMask) > 0)*destinationBitMask
973 """Clear the mask plane of an exposure.
977 mask : `lsst.afw.image.Mask`
978 The mask plane to erase, which will be modified in place.
979 clearMaskPlanes : `list` of `str`, optional
980 Erase the specified mask planes.
981 If not supplied, the entire mask will be erased.
983 if clearMaskPlanes
is None:
984 clearMaskPlanes = list(mask.getMaskPlaneDict().keys())
986 bitMaskToClear = mask.getPlaneBitMask(clearMaskPlanes)
987 mask &= ~bitMaskToClear
991 SubtractScoreOutputConnections):
996 pipelineConnections=AlardLuptonPreconvolveSubtractConnections):
1001 """Subtract a template from a science image, convolving the science image
1002 before computing the kernel, and also convolving the template before
1005 ConfigClass = AlardLuptonPreconvolveSubtractConfig
1006 _DefaultName =
"alardLuptonPreconvolveSubtract"
1008 def run(self, template, science, sources, visitSummary=None):
1009 """Preconvolve the science image with its own PSF,
1010 convolve the template image with a PSF-matching kernel and subtract
1011 from the preconvolved science image.
1015 template : `lsst.afw.image.ExposureF`
1016 The template image, which has previously been warped to the science
1017 image. The template bbox will be padded by a few pixels compared to
1019 science : `lsst.afw.image.ExposureF`
1020 The science exposure.
1021 sources : `lsst.afw.table.SourceCatalog`
1022 Identified sources on the science exposure. This catalog is used to
1023 select sources in order to perform the AL PSF matching on stamp
1025 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
1026 Exposure catalog with complete external calibrations. Catalog uses
1027 the detector id for the catalog id, sorted on id for fast lookup.
1031 results : `lsst.pipe.base.Struct`
1032 ``scoreExposure`` : `lsst.afw.image.ExposureF`
1033 Result of subtracting the convolved template and science
1034 images. Attached PSF is that of the original science image.
1035 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
1036 Warped and PSF-matched template exposure. Attached PSF is that
1037 of the original science image.
1038 ``matchedScience`` : `lsst.afw.image.ExposureF`
1039 The science exposure after convolving with its own PSF.
1040 Attached PSF is that of the original science image.
1041 ``backgroundModel`` : `lsst.afw.math.Function2D`
1042 Background model that was fit while solving for the
1044 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
1045 Final kernel used to PSF-match the template to the science
1048 self.
_prepareInputs(template, science, visitSummary=visitSummary)
1051 scienceKernel = science.psf.getKernel()
1053 interpolateBadMaskPlanes=
True)
1054 self.metadata[
"convolvedExposure"] =
"Preconvolution"
1057 subtractResults = self.
runPreconvolve(template, science, matchedScience,
1058 selectSources, scienceKernel)
1061 self.
loglog.warning(
"Failed to match template. Checking coverage")
1064 self.config.minTemplateFractionForExpectedSuccess,
1065 exceptionMessage=
"Template coverage lower than expected to succeed."
1066 f
" Failure is tolerable: {e}")
1070 return subtractResults
1072 def runPreconvolve(self, template, science, matchedScience, selectSources, preConvKernel):
1073 """Convolve the science image with its own PSF, then convolve the
1074 template with a matching kernel and subtract to form the Score
1079 template : `lsst.afw.image.ExposureF`
1080 Template exposure, warped to match the science exposure.
1081 science : `lsst.afw.image.ExposureF`
1082 Science exposure to subtract from the template.
1083 matchedScience : `lsst.afw.image.ExposureF`
1084 The science exposure, convolved with the reflection of its own PSF.
1085 selectSources : `lsst.afw.table.SourceCatalog`
1086 Identified sources on the science exposure. This catalog is used to
1087 select sources in order to perform the AL PSF matching on stamp
1089 preConvKernel : `lsst.afw.math.Kernel`
1090 The reflection of the kernel that was used to preconvolve the
1091 `science` exposure. Must be normalized to sum to 1.
1095 results : `lsst.pipe.base.Struct`
1097 ``scoreExposure`` : `lsst.afw.image.ExposureF`
1098 Result of subtracting the convolved template and science
1099 images. Attached PSF is that of the original science image.
1100 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
1101 Warped and PSF-matched template exposure. Attached PSF is that
1102 of the original science image.
1103 ``matchedScience`` : `lsst.afw.image.ExposureF`
1104 The science exposure after convolving with its own PSF.
1105 Attached PSF is that of the original science image.
1106 ``backgroundModel`` : `lsst.afw.math.Function2D`
1107 Background model that was fit while solving for the
1109 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
1110 Final kernel used to PSF-match the template to the science
1113 bbox = science.getBBox()
1114 innerBBox = preConvKernel.shrinkBBox(bbox)
1116 kernelSources = self.makeKernel.selectKernelSources(template[innerBBox], matchedScience[innerBBox],
1117 candidateList=selectSources,
1119 kernelResult = self.makeKernel.run(template[innerBBox], matchedScience[innerBBox], kernelSources,
1122 matchedTemplate = self.
_convolveExposure(template, kernelResult.psfMatchingKernel,
1126 interpolateBadMaskPlanes=
True,
1127 photoCalib=science.photoCalib)
1129 backgroundModel=(kernelResult.backgroundModel
1130 if self.config.doSubtractBackground
else None))
1131 correctedScore = self.
finalize(template[bbox], science, score,
1132 kernelResult.psfMatchingKernel,
1133 templateMatched=
True, preConvMode=
True,
1134 preConvKernel=preConvKernel)
1136 return lsst.pipe.base.Struct(scoreExposure=correctedScore,
1137 matchedTemplate=matchedTemplate,
1138 matchedScience=matchedScience,
1139 backgroundModel=kernelResult.backgroundModel,
1140 psfMatchingKernel=kernelResult.psfMatchingKernel,
1141 kernelSources=kernelSources)
1145 exceptionMessage=""):
1146 """Raise NoWorkFound if template coverage < requiredTemplateFraction
1150 templateExposure : `lsst.afw.image.ExposureF`
1151 The template exposure to check
1152 logger : `logging.Logger`
1153 Logger for printing output.
1154 requiredTemplateFraction : `float`, optional
1155 Fraction of pixels of the science image required to have coverage
1157 exceptionMessage : `str`, optional
1158 Message to include in the exception raised if the template coverage
1163 templateCoverageFraction: `float`
1164 Fraction of pixels in the template with data.
1168 lsst.pipe.base.NoWorkFound
1169 Raised if fraction of good pixels, defined as not having NO_DATA
1170 set, is less than the requiredTemplateFraction
1174 pixNoData = np.count_nonzero(templateExposure.mask.array
1175 & templateExposure.mask.getPlaneBitMask(
'NO_DATA'))
1176 pixGood = templateExposure.getBBox().getArea() - pixNoData
1177 templateCoverageFraction = pixGood/templateExposure.getBBox().getArea()
1178 logger.info(
"template has %d good pixels (%.1f%%)", pixGood, 100*templateCoverageFraction)
1180 if templateCoverageFraction < requiredTemplateFraction:
1181 message = (
"Insufficient Template Coverage. (%.1f%% < %.1f%%)" % (
1182 100*templateCoverageFraction,
1183 100*requiredTemplateFraction))
1184 raise lsst.pipe.base.NoWorkFound(message +
" " + exceptionMessage)
1185 return templateCoverageFraction
1189 """Subtract template from science, propagating relevant metadata.
1193 science : `lsst.afw.Exposure`
1194 The input science image.
1195 template : `lsst.afw.Exposure`
1196 The template to subtract from the science image.
1197 backgroundModel : `lsst.afw.MaskedImage`, optional
1198 Differential background model
1202 difference : `lsst.afw.Exposure`
1203 The subtracted image.
1205 difference = science.clone()
1206 if backgroundModel
is not None:
1207 difference.maskedImage -= backgroundModel
1208 difference.maskedImage -= template.maskedImage
1213 """Determine that the PSF of ``exp1`` is not wider than that of ``exp2``.
1217 exp1 : `~lsst.afw.image.Exposure`
1218 Exposure with the reference point spread function (PSF) to evaluate.
1219 exp2 : `~lsst.afw.image.Exposure`
1220 Exposure with a candidate point spread function (PSF) to evaluate.
1221 fwhmExposureBuffer : `float`
1222 Fractional buffer margin to be left out of all sides of the image
1223 during the construction of the grid to compute mean PSF FWHM in an
1224 exposure, if the PSF is not available at its average position.
1225 fwhmExposureGrid : `int`
1226 Grid size to compute the mean FWHM in an exposure, if the PSF is not
1227 available at its average position.
1231 True if ``exp1`` has a PSF that is not wider than that of ``exp2`` in
1235 shape1 = getPsfFwhm(exp1.psf, average=
False)
1236 shape2 = getPsfFwhm(exp2.psf, average=
False)
1238 shape1 = evaluateMeanPsfFwhm(exp1,
1239 fwhmExposureBuffer=fwhmExposureBuffer,
1240 fwhmExposureGrid=fwhmExposureGrid
1242 shape2 = evaluateMeanPsfFwhm(exp2,
1243 fwhmExposureBuffer=fwhmExposureBuffer,
1244 fwhmExposureGrid=fwhmExposureGrid
1246 return shape1 <= shape2
1249 xTest = shape1[0] <= shape2[0]
1250 yTest = shape1[1] <= shape2[1]
1251 return xTest | yTest
1255 """Replace masked image pixels with interpolated values.
1259 maskedImage : `lsst.afw.image.MaskedImage`
1260 Image on which to perform interpolation.
1261 badMaskPlanes : `list` of `str`
1262 List of mask planes to interpolate over.
1263 fallbackValue : `float`, optional
1264 Value to set when interpolation fails.
1269 The number of masked pixels that were replaced.
1271 imgBadMaskPlanes = [
1272 maskPlane
for maskPlane
in badMaskPlanes
if maskPlane
in maskedImage.mask.getMaskPlaneDict()
1275 image = maskedImage.image.array
1276 badPixels = (maskedImage.mask.array & maskedImage.mask.getPlaneBitMask(imgBadMaskPlanes)) > 0
1277 image[badPixels] = np.nan
1278 if fallbackValue
is None:
1279 fallbackValue = np.nanmedian(image)
1282 image[badPixels] = fallbackValue
1283 return np.sum(badPixels)
Parameters to control convolution.
runPreconvolve(self, template, science, matchedScience, selectSources, preConvKernel)
run(self, template, science, sources, visitSummary=None)
_clearMask(self, mask, clearMaskPlanes=None)
_prepareInputs(self, template, science, visitSummary=None)
run(self, template, science, sources, visitSummary=None)
_calculateMagLim(self, exposure, nsigma=5.0, fallbackPsfSize=None)
runConvolveTemplate(self, template, science, selectSources)
_applyExternalCalibrations(self, exposure, visitSummary)
_checkMask(mask, sources, excludeMaskPlanes, checkAdjacent=True)
updateMasks(self, template, science)
_sourceSelector(self, sources, mask)
_convolveExposure(self, exposure, kernel, convolutionControl, bbox=None, psf=None, photoCalib=None, interpolateBadMaskPlanes=False)
runConvolveScience(self, template, science, selectSources)
finalize(self, template, science, difference, kernel, templateMatched=True, preConvMode=False, preConvKernel=None, spatiallyVarying=False)
_validateExposures(template, science)
_renameMaskPlanes(mask, maskPlane, newMaskPlane)
Provides consistent interface for LSST exceptions.
Reports invalid arguments.
Reports when the result of an operation cannot be represented by the destination type.
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, ConvolutionControl const &convolutionControl=ConvolutionControl())
Convolve an Image or MaskedImage with a Kernel, setting pixels of an existing output image.
_subtractImages(science, template, backgroundModel=None)
_interpolateImage(maskedImage, badMaskPlanes, fallbackValue=None)
checkTemplateIsSufficient(templateExposure, logger, requiredTemplateFraction=0., exceptionMessage="")
_shapeTest(exp1, exp2, fwhmExposureBuffer, fwhmExposureGrid)