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 doc=
"Flags that, if set, the associated source should not "
218 "be used to determine the PSF matching kernel.",
219 default=(
"sky_source",
"slot_Centroid_flag",
220 "slot_ApFlux_flag",
"slot_PsfFlux_flag",
221 "base_PixelFlags_flag_interpolated",
222 "base_PixelFlags_flag_saturated",
223 "base_PixelFlags_flag_bad",
225 deprecated=
"This field is no longer used and will be removed after v28."
226 "Set the equivalent field for the sourceSelector subtask instead.",
230 default=(
"NO_DATA",
"BAD",
"SAT",
"EDGE",
"FAKE"),
231 doc=
"Mask planes to exclude when selecting sources for PSF matching."
235 default=(
"NO_DATA",
"BAD",
"SAT",
"EDGE"),
236 doc=
"Mask planes to interpolate over."
240 default=(
"NO_DATA",
"BAD",),
241 doc=
"Mask planes from the template to propagate to the image difference."
245 default=(
"SAT",
"INJECTED",
"INJECTED_CORE",),
246 doc=
"Mask planes from the template to propagate to the image difference"
247 "with '_TEMPLATE' appended to the name."
252 doc=
"Re-run source detection for kernel candidates if an error is"
253 " encountered while calculating the matching kernel."
259 self.
makeKernel.kernel.active.spatialKernelOrder = 1
260 self.
makeKernel.kernel.active.spatialBgOrder = 2
271 pipelineConnections=AlardLuptonSubtractConnections):
274 default=
"convolveTemplate",
275 allowed={
"auto":
"Choose which image to convolve at runtime.",
276 "convolveScience":
"Only convolve the science image.",
277 "convolveTemplate":
"Only convolve the template image."},
278 doc=
"Choose which image to convolve at runtime, or require that a specific image is convolved."
283 """Compute the image difference of a science and template image using
284 the Alard & Lupton (1998) algorithm.
286 ConfigClass = AlardLuptonSubtractConfig
287 _DefaultName =
"alardLuptonSubtract"
291 self.makeSubtask(
"decorrelate")
292 self.makeSubtask(
"makeKernel")
293 self.makeSubtask(
"sourceSelector")
294 if self.config.doScaleVariance:
295 self.makeSubtask(
"scaleVariance")
304 """Replace calibrations (psf, and ApCorrMap) on this exposure with
309 exposure : `lsst.afw.image.exposure.Exposure`
310 Input exposure to adjust calibrations.
311 visitSummary : `lsst.afw.table.ExposureCatalog`
312 Exposure catalog with external calibrations to be applied. Catalog
313 uses the detector id for the catalog id, sorted on id for fast
318 exposure : `lsst.afw.image.exposure.Exposure`
319 Exposure with adjusted calibrations.
321 detectorId = exposure.info.getDetector().getId()
323 row = visitSummary.find(detectorId)
325 self.
log.warning(
"Detector id %s not found in external calibrations catalog; "
326 "Using original calibrations.", detectorId)
329 apCorrMap = row.getApCorrMap()
331 self.
log.warning(
"Detector id %s has None for psf in "
332 "external calibrations catalog; Using original psf and aperture correction.",
334 elif apCorrMap
is None:
335 self.
log.warning(
"Detector id %s has None for apCorrMap in "
336 "external calibrations catalog; Using original psf and aperture correction.",
340 exposure.info.setApCorrMap(apCorrMap)
345 def run(self, template, science, sources, visitSummary=None):
346 """PSF match, subtract, and decorrelate two images.
350 template : `lsst.afw.image.ExposureF`
351 Template exposure, warped to match the science exposure.
352 science : `lsst.afw.image.ExposureF`
353 Science exposure to subtract from the template.
354 sources : `lsst.afw.table.SourceCatalog`
355 Identified sources on the science exposure. This catalog is used to
356 select sources in order to perform the AL PSF matching on stamp
358 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
359 Exposure catalog with external calibrations to be applied. Catalog
360 uses the detector id for the catalog id, sorted on id for fast
365 results : `lsst.pipe.base.Struct`
366 ``difference`` : `lsst.afw.image.ExposureF`
367 Result of subtracting template and science.
368 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
369 Warped and PSF-matched template exposure.
370 ``backgroundModel`` : `lsst.afw.math.Function2D`
371 Background model that was fit while solving for the
373 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
374 Kernel used to PSF-match the convolved image.
379 If an unsupported convolution mode is supplied.
381 If there are too few sources to calculate the PSF matching kernel.
382 lsst.pipe.base.NoWorkFound
383 Raised if fraction of good pixels, defined as not having NO_DATA
384 set, is less then the configured requiredTemplateFraction
390 fwhmExposureBuffer = self.config.makeKernel.fwhmExposureBuffer
391 fwhmExposureGrid = self.config.makeKernel.fwhmExposureGrid
405 templatePsfSize = getPsfFwhm(template.psf)
406 sciencePsfSize = getPsfFwhm(science.psf)
408 self.
log.info(
"Unable to evaluate PSF at the average position. "
409 "Evaluting PSF on a grid of points."
411 templatePsfSize = evaluateMeanPsfFwhm(template,
412 fwhmExposureBuffer=fwhmExposureBuffer,
413 fwhmExposureGrid=fwhmExposureGrid
415 sciencePsfSize = evaluateMeanPsfFwhm(science,
416 fwhmExposureBuffer=fwhmExposureBuffer,
417 fwhmExposureGrid=fwhmExposureGrid
419 self.
log.info(
"Science PSF FWHM: %f pixels", sciencePsfSize)
420 self.
log.info(
"Template PSF FWHM: %f pixels", templatePsfSize)
421 self.metadata[
"sciencePsfSize"] = sciencePsfSize
422 self.metadata[
"templatePsfSize"] = templatePsfSize
425 maglim_science = self.
_calculateMagLim(science, fallbackPsfSize=sciencePsfSize)
426 fluxlim_science = (maglim_science*u.ABmag).to_value(u.nJy)
427 maglim_template = self.
_calculateMagLim(template, fallbackPsfSize=templatePsfSize)
428 if np.isnan(maglim_template):
429 self.
log.info(
"Cannot evaluate template limiting mag; adopting science limiting mag for diffim")
430 maglim_diffim = maglim_science
432 fluxlim_template = (maglim_template*u.ABmag).to_value(u.nJy)
433 maglim_diffim = (np.sqrt(fluxlim_science**2 + fluxlim_template**2)*u.nJy).to(u.ABmag).value
434 self.metadata[
"scienceLimitingMagnitude"] = maglim_science
435 self.metadata[
"templateLimitingMagnitude"] = maglim_template
436 self.metadata[
"diffimLimitingMagnitude"] = maglim_diffim
438 if self.config.mode ==
"auto":
441 fwhmExposureBuffer=fwhmExposureBuffer,
442 fwhmExposureGrid=fwhmExposureGrid)
444 if sciencePsfSize < templatePsfSize:
445 self.
log.info(
"Average template PSF size is greater, "
446 "but science PSF greater in one dimension: convolving template image.")
448 self.
log.info(
"Science PSF size is greater: convolving template image.")
450 self.
log.info(
"Template PSF size is greater: convolving science image.")
451 elif self.config.mode ==
"convolveTemplate":
452 self.
log.info(
"`convolveTemplate` is set: convolving template image.")
453 convolveTemplate =
True
454 elif self.config.mode ==
"convolveScience":
455 self.
log.info(
"`convolveScience` is set: convolving science image.")
456 convolveTemplate =
False
458 raise RuntimeError(
"Cannot handle AlardLuptonSubtract mode: %s", self.config.mode)
461 sourceMask = science.mask.clone()
462 sourceMask.array |= template[science.getBBox()].mask.array
465 self.metadata[
"convolvedExposure"] =
"Template"
468 self.metadata[
"convolvedExposure"] =
"Science"
472 self.
log.warning(
"Failed to match template. Checking coverage")
475 self.config.minTemplateFractionForExpectedSuccess,
476 exceptionMessage=
"Template coverage lower than expected to succeed."
477 f
" Failure is tolerable: {e}")
481 return subtractResults
484 """Convolve the template image with a PSF-matching kernel and subtract
485 from the science image.
489 template : `lsst.afw.image.ExposureF`
490 Template exposure, warped to match the science exposure.
491 science : `lsst.afw.image.ExposureF`
492 Science exposure to subtract from the template.
493 selectSources : `lsst.afw.table.SourceCatalog`
494 Identified sources on the science exposure. This catalog is used to
495 select sources in order to perform the AL PSF matching on stamp
500 results : `lsst.pipe.base.Struct`
502 ``difference`` : `lsst.afw.image.ExposureF`
503 Result of subtracting template and science.
504 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
505 Warped and PSF-matched template exposure.
506 ``backgroundModel`` : `lsst.afw.math.Function2D`
507 Background model that was fit while solving for the PSF-matching kernel
508 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
509 Kernel used to PSF-match the template to the science image.
512 kernelSources = self.makeKernel.selectKernelSources(template, science,
513 candidateList=selectSources,
515 kernelResult = self.makeKernel.run(template, science, kernelSources,
517 except Exception
as e:
518 if self.config.allowKernelSourceDetection:
519 self.
log.warning(
"Error encountered trying to construct the matching kernel"
520 f
" Running source detection and retrying. {e}")
521 kernelSources = self.makeKernel.selectKernelSources(template, science,
524 kernelResult = self.makeKernel.run(template, science, kernelSources,
529 matchedTemplate = self.
_convolveExposure(template, kernelResult.psfMatchingKernel,
531 bbox=science.getBBox(),
533 photoCalib=science.photoCalib)
536 backgroundModel=(kernelResult.backgroundModel
537 if self.config.doSubtractBackground
else None))
538 correctedExposure = self.
finalize(template, science, difference,
539 kernelResult.psfMatchingKernel,
540 templateMatched=
True)
542 return lsst.pipe.base.Struct(difference=correctedExposure,
543 matchedTemplate=matchedTemplate,
544 matchedScience=science,
545 backgroundModel=kernelResult.backgroundModel,
546 psfMatchingKernel=kernelResult.psfMatchingKernel,
547 kernelSources=kernelSources)
550 """Convolve the science image with a PSF-matching kernel and subtract
555 template : `lsst.afw.image.ExposureF`
556 Template exposure, warped to match the science exposure.
557 science : `lsst.afw.image.ExposureF`
558 Science exposure to subtract from the template.
559 selectSources : `lsst.afw.table.SourceCatalog`
560 Identified sources on the science exposure. This catalog is used to
561 select sources in order to perform the AL PSF matching on stamp
566 results : `lsst.pipe.base.Struct`
568 ``difference`` : `lsst.afw.image.ExposureF`
569 Result of subtracting template and science.
570 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
571 Warped template exposure. Note that in this case, the template
572 is not PSF-matched to the science image.
573 ``backgroundModel`` : `lsst.afw.math.Function2D`
574 Background model that was fit while solving for the PSF-matching kernel
575 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
576 Kernel used to PSF-match the science image to the template.
578 bbox = science.getBBox()
579 kernelSources = self.makeKernel.selectKernelSources(science, template,
580 candidateList=selectSources,
582 kernelResult = self.makeKernel.run(science, template, kernelSources,
584 modelParams = kernelResult.backgroundModel.getParameters()
586 kernelResult.backgroundModel.setParameters([-p
for p
in modelParams])
588 kernelImage = lsst.afw.image.ImageD(kernelResult.psfMatchingKernel.getDimensions())
589 norm = kernelResult.psfMatchingKernel.computeImage(kernelImage, doNormalize=
False)
596 matchedScience.maskedImage /= norm
597 matchedTemplate = template.clone()[bbox]
598 matchedTemplate.maskedImage /= norm
599 matchedTemplate.setPhotoCalib(science.photoCalib)
602 backgroundModel=(kernelResult.backgroundModel
603 if self.config.doSubtractBackground
else None))
605 correctedExposure = self.
finalize(template, science, difference,
606 kernelResult.psfMatchingKernel,
607 templateMatched=
False)
609 return lsst.pipe.base.Struct(difference=correctedExposure,
610 matchedTemplate=matchedTemplate,
611 matchedScience=matchedScience,
612 backgroundModel=kernelResult.backgroundModel,
613 psfMatchingKernel=kernelResult.psfMatchingKernel,
614 kernelSources=kernelSources)
616 def finalize(self, template, science, difference, kernel,
617 templateMatched=True,
620 spatiallyVarying=False):
621 """Decorrelate the difference image to undo the noise correlations
622 caused by convolution.
626 template : `lsst.afw.image.ExposureF`
627 Template exposure, warped to match the science exposure.
628 science : `lsst.afw.image.ExposureF`
629 Science exposure to subtract from the template.
630 difference : `lsst.afw.image.ExposureF`
631 Result of subtracting template and science.
632 kernel : `lsst.afw.math.Kernel`
633 An (optionally spatially-varying) PSF matching kernel
634 templateMatched : `bool`, optional
635 Was the template PSF-matched to the science image?
636 preConvMode : `bool`, optional
637 Was the science image preconvolved with its own PSF
638 before PSF matching the template?
639 preConvKernel : `lsst.afw.detection.Psf`, optional
640 If not `None`, then the science image was pre-convolved with
641 (the reflection of) this kernel. Must be normalized to sum to 1.
642 spatiallyVarying : `bool`, optional
643 Compute the decorrelation kernel spatially varying across the image?
647 correctedExposure : `lsst.afw.image.ExposureF`
648 The decorrelated image difference.
650 if self.config.doDecorrelation:
651 self.
log.info(
"Decorrelating image difference.")
655 correctedExposure = self.decorrelate.run(science, template[science.getBBox()], difference, kernel,
656 templateMatched=templateMatched,
657 preConvMode=preConvMode,
658 preConvKernel=preConvKernel,
659 spatiallyVarying=spatiallyVarying).correctedExposure
661 self.
log.info(
"NOT decorrelating image difference.")
662 correctedExposure = difference
663 return correctedExposure
666 """Calculate an exposure's limiting magnitude.
668 This method uses the photometric zeropoint together with the
669 PSF size from the average position of the exposure.
673 exposure : `lsst.afw.image.Exposure`
674 The target exposure to calculate the limiting magnitude for.
675 nsigma : `float`, optional
676 The detection threshold in sigma.
677 fallbackPsfSize : `float`, optional
678 PSF FWHM to use in the event the exposure PSF cannot be retrieved.
682 maglim : `astropy.units.Quantity`
683 The limiting magnitude of the exposure, or np.nan.
686 psf = exposure.getPsf()
687 psf_shape = psf.computeShape(psf.getAveragePosition())
689 if fallbackPsfSize
is not None:
690 self.
log.info(
"Unable to evaluate PSF, using fallback FWHM %f", fallbackPsfSize)
691 psf_area = np.pi*(fallbackPsfSize/2)**2
692 zeropoint = exposure.getPhotoCalib().instFluxToMagnitude(1)
693 maglim = zeropoint - 2.5*np.log10(nsigma*np.sqrt(psf_area))
695 self.
log.info(
"Unable to evaluate PSF, setting maglim to nan")
699 psf_area = np.pi*np.sqrt(psf_shape.getIxx()*psf_shape.getIyy())
700 zeropoint = exposure.getPhotoCalib().instFluxToMagnitude(1)
701 maglim = zeropoint - 2.5*np.log10(nsigma*np.sqrt(psf_area))
707 """Check that the WCS of the two Exposures match, the template bbox
708 contains the science bbox, and that the bands match.
712 template : `lsst.afw.image.ExposureF`
713 Template exposure, warped to match the science exposure.
714 science : `lsst.afw.image.ExposureF`
715 Science exposure to subtract from the template.
720 Raised if the WCS of the template is not equal to the science WCS,
721 if the science image is not fully contained in the template
722 bounding box, or if the bands do not match.
724 assert template.wcs == science.wcs, \
725 "Template and science exposure WCS are not identical."
726 templateBBox = template.getBBox()
727 scienceBBox = science.getBBox()
728 assert science.filter.bandLabel == template.filter.bandLabel, \
729 "Science and template exposures have different bands: %s, %s" % \
730 (science.filter, template.filter)
732 assert templateBBox.contains(scienceBBox), \
733 "Template bbox does not contain all of the science image."
739 interpolateBadMaskPlanes=False,
741 """Convolve an exposure with the given kernel.
745 exposure : `lsst.afw.Exposure`
746 exposure to convolve.
747 kernel : `lsst.afw.math.LinearCombinationKernel`
748 PSF matching kernel computed in the ``makeKernel`` subtask.
749 convolutionControl : `lsst.afw.math.ConvolutionControl`
750 Configuration for convolve algorithm.
751 bbox : `lsst.geom.Box2I`, optional
752 Bounding box to trim the convolved exposure to.
753 psf : `lsst.afw.detection.Psf`, optional
754 Point spread function (PSF) to set for the convolved exposure.
755 photoCalib : `lsst.afw.image.PhotoCalib`, optional
756 Photometric calibration of the convolved exposure.
760 convolvedExp : `lsst.afw.Exposure`
763 convolvedExposure = exposure.clone()
765 convolvedExposure.setPsf(psf)
766 if photoCalib
is not None:
767 convolvedExposure.setPhotoCalib(photoCalib)
768 if interpolateBadMaskPlanes
and self.config.badMaskPlanes
is not None:
770 self.config.badMaskPlanes)
771 self.metadata[
"nInterpolated"] = nInterp
772 convolvedImage = lsst.afw.image.MaskedImageF(convolvedExposure.getBBox())
774 convolvedExposure.setMaskedImage(convolvedImage)
776 return convolvedExposure
778 return convolvedExposure[bbox]
781 """Select sources from a catalog that meet the selection criteria.
785 sources : `lsst.afw.table.SourceCatalog`
786 Input source catalog to select sources from.
787 mask : `lsst.afw.image.Mask`
788 The image mask plane to use to reject sources
789 based on their location on the ccd.
793 selectSources : `lsst.afw.table.SourceCatalog`
794 The input source catalog, with flagged and low signal-to-noise
800 If there are too few sources to compute the PSF matching kernel
801 remaining after source selection.
804 selected = self.sourceSelector.selectSources(sources).selected
805 nInitialSelected = np.count_nonzero(selected)
806 selected *= self.
_checkMask(mask, sources, self.config.excludeMaskPlanes)
807 nSelected = np.count_nonzero(selected)
808 self.
log.info(
"Rejecting %i candidate sources: an excluded template mask plane is set.",
809 nInitialSelected - nSelected)
810 selectSources = sources[selected].copy(deep=
True)
813 if (len(selectSources) > self.config.maxKernelSources) & (self.config.maxKernelSources > 0):
814 signalToNoise = selectSources.getPsfInstFlux()/selectSources.getPsfInstFluxErr()
815 indices = np.argsort(signalToNoise)
816 indices = indices[-self.config.maxKernelSources:]
817 selected = np.zeros(len(selectSources), dtype=bool)
818 selected[indices] =
True
819 selectSources = selectSources[selected].copy(deep=
True)
821 self.
log.info(
"%i/%i=%.1f%% of sources selected for PSF matching from the input catalog",
822 len(selectSources), len(sources), 100*len(selectSources)/len(sources))
823 if len(selectSources) < self.config.minKernelSources:
824 self.
log.error(
"Too few sources to calculate the PSF matching kernel: "
825 "%i selected but %i needed for the calculation.",
826 len(selectSources), self.config.minKernelSources)
827 if not self.config.allowKernelSourceDetection:
828 raise RuntimeError(
"Cannot compute PSF matching kernel: too few sources selected.")
829 self.metadata[
"nPsfSources"] = len(selectSources)
834 def _checkMask(mask, sources, excludeMaskPlanes, checkAdjacent=True):
835 """Exclude sources that are located on or adjacent to masked pixels.
839 mask : `lsst.afw.image.Mask`
840 The image mask plane to use to reject sources
841 based on the location of their centroid on the ccd.
842 sources : `lsst.afw.table.SourceCatalog`
843 The source catalog to evaluate.
844 excludeMaskPlanes : `list` of `str`
845 List of the names of the mask planes to exclude.
849 flags : `numpy.ndarray` of `bool`
850 Array indicating whether each source in the catalog should be
851 kept (True) or rejected (False) based on the value of the
852 mask plane at its location.
854 setExcludeMaskPlanes = [
855 maskPlane
for maskPlane
in excludeMaskPlanes
if maskPlane
in mask.getMaskPlaneDict()
858 excludePixelMask = mask.getPlaneBitMask(setExcludeMaskPlanes)
860 xv = (np.rint(sources.getX() - mask.getX0())).astype(int)
861 yv = (np.rint(sources.getY() - mask.getY0())).astype(int)
863 flags = np.ones(len(sources), dtype=bool)
865 pixRange = (0, -1, 1)
870 mv = mask.array[yv + j, xv + i]
871 flags *= np.bitwise_and(mv, excludePixelMask) == 0
875 """Perform preparatory calculations common to all Alard&Lupton Tasks.
879 template : `lsst.afw.image.ExposureF`
880 Template exposure, warped to match the science exposure. The
881 variance plane of the template image is modified in place.
882 science : `lsst.afw.image.ExposureF`
883 Science exposure to subtract from the template. The variance plane
884 of the science image is modified in place.
885 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
886 Exposure catalog with external calibrations to be applied. Catalog
887 uses the detector id for the catalog id, sorted on id for fast
891 if visitSummary
is not None:
894 template[science.getBBox()], self.
log,
895 requiredTemplateFraction=self.config.requiredTemplateFraction,
896 exceptionMessage=
"Not attempting subtraction. To force subtraction,"
897 " set config requiredTemplateFraction=0"
899 self.metadata[
"templateCoveragePercent"] = 100*templateCoverageFraction
901 if self.config.doScaleVariance:
905 templateVarFactor = self.scaleVariance.run(template.maskedImage)
906 sciVarFactor = self.scaleVariance.run(science.maskedImage)
907 self.
log.info(
"Template variance scaling factor: %.2f", templateVarFactor)
908 self.metadata[
"scaleTemplateVarianceFactor"] = templateVarFactor
909 self.
log.info(
"Science variance scaling factor: %.2f", sciVarFactor)
910 self.metadata[
"scaleScienceVarianceFactor"] = sciVarFactor
917 """Update the science and template mask planes before differencing.
921 template : `lsst.afw.image.Exposure`
922 Template exposure, warped to match the science exposure.
923 The template mask planes will be erased, except for a few specified
925 science : `lsst.afw.image.Exposure`
926 Science exposure to subtract from the template.
927 The DETECTED and DETECTED_NEGATIVE mask planes of the science image
930 self.
_clearMask(science.mask, clearMaskPlanes=[
"DETECTED",
"DETECTED_NEGATIVE"])
937 clearMaskPlanes = [mp
for mp
in template.mask.getMaskPlaneDict().keys()
938 if mp
not in self.config.preserveTemplateMask]
939 renameMaskPlanes = [mp
for mp
in self.config.renameTemplateMask
940 if mp
in template.mask.getMaskPlaneDict().keys()]
945 if "FAKE" in science.mask.getMaskPlaneDict().keys():
946 self.
log.info(
"Adding injected mask plane to science image")
948 if "FAKE" in template.mask.getMaskPlaneDict().keys():
949 self.
log.info(
"Adding injected mask plane to template image")
951 if "INJECTED" in renameMaskPlanes:
952 renameMaskPlanes.remove(
"INJECTED")
953 if "INJECTED_TEMPLATE" in clearMaskPlanes:
954 clearMaskPlanes.remove(
"INJECTED_TEMPLATE")
956 for maskPlane
in renameMaskPlanes:
958 self.
_clearMask(template.mask, clearMaskPlanes=clearMaskPlanes)
962 """Rename a mask plane by adding the new name and copying the data.
966 mask : `lsst.afw.image.Mask`
967 The mask image to update in place.
969 The name of the existing mask plane to copy.
971 The new name of the mask plane that will be added.
972 If the mask plane already exists, it will be updated in place.
974 mask.addMaskPlane(newMaskPlane)
975 originBitMask = mask.getPlaneBitMask(maskPlane)
976 destinationBitMask = mask.getPlaneBitMask(newMaskPlane)
977 mask.array |= ((mask.array & originBitMask) > 0)*destinationBitMask
980 """Clear the mask plane of an exposure.
984 mask : `lsst.afw.image.Mask`
985 The mask plane to erase, which will be modified in place.
986 clearMaskPlanes : `list` of `str`, optional
987 Erase the specified mask planes.
988 If not supplied, the entire mask will be erased.
990 if clearMaskPlanes
is None:
991 clearMaskPlanes = list(mask.getMaskPlaneDict().keys())
993 bitMaskToClear = mask.getPlaneBitMask(clearMaskPlanes)
994 mask &= ~bitMaskToClear
998 SubtractScoreOutputConnections):
1003 pipelineConnections=AlardLuptonPreconvolveSubtractConnections):
1008 """Subtract a template from a science image, convolving the science image
1009 before computing the kernel, and also convolving the template before
1012 ConfigClass = AlardLuptonPreconvolveSubtractConfig
1013 _DefaultName =
"alardLuptonPreconvolveSubtract"
1015 def run(self, template, science, sources, visitSummary=None):
1016 """Preconvolve the science image with its own PSF,
1017 convolve the template image with a PSF-matching kernel and subtract
1018 from the preconvolved science image.
1022 template : `lsst.afw.image.ExposureF`
1023 The template image, which has previously been warped to the science
1024 image. The template bbox will be padded by a few pixels compared to
1026 science : `lsst.afw.image.ExposureF`
1027 The science exposure.
1028 sources : `lsst.afw.table.SourceCatalog`
1029 Identified sources on the science exposure. This catalog is used to
1030 select sources in order to perform the AL PSF matching on stamp
1032 visitSummary : `lsst.afw.table.ExposureCatalog`, optional
1033 Exposure catalog with complete external calibrations. Catalog uses
1034 the detector id for the catalog id, sorted on id for fast lookup.
1038 results : `lsst.pipe.base.Struct`
1039 ``scoreExposure`` : `lsst.afw.image.ExposureF`
1040 Result of subtracting the convolved template and science
1041 images. Attached PSF is that of the original science image.
1042 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
1043 Warped and PSF-matched template exposure. Attached PSF is that
1044 of the original science image.
1045 ``matchedScience`` : `lsst.afw.image.ExposureF`
1046 The science exposure after convolving with its own PSF.
1047 Attached PSF is that of the original science image.
1048 ``backgroundModel`` : `lsst.afw.math.Function2D`
1049 Background model that was fit while solving for the
1051 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
1052 Final kernel used to PSF-match the template to the science
1055 self.
_prepareInputs(template, science, visitSummary=visitSummary)
1058 scienceKernel = science.psf.getKernel()
1060 interpolateBadMaskPlanes=
True)
1061 self.metadata[
"convolvedExposure"] =
"Preconvolution"
1064 subtractResults = self.
runPreconvolve(template, science, matchedScience,
1065 selectSources, scienceKernel)
1068 self.
loglog.warning(
"Failed to match template. Checking coverage")
1071 self.config.minTemplateFractionForExpectedSuccess,
1072 exceptionMessage=
"Template coverage lower than expected to succeed."
1073 f
" Failure is tolerable: {e}")
1077 return subtractResults
1079 def runPreconvolve(self, template, science, matchedScience, selectSources, preConvKernel):
1080 """Convolve the science image with its own PSF, then convolve the
1081 template with a matching kernel and subtract to form the Score
1086 template : `lsst.afw.image.ExposureF`
1087 Template exposure, warped to match the science exposure.
1088 science : `lsst.afw.image.ExposureF`
1089 Science exposure to subtract from the template.
1090 matchedScience : `lsst.afw.image.ExposureF`
1091 The science exposure, convolved with the reflection of its own PSF.
1092 selectSources : `lsst.afw.table.SourceCatalog`
1093 Identified sources on the science exposure. This catalog is used to
1094 select sources in order to perform the AL PSF matching on stamp
1096 preConvKernel : `lsst.afw.math.Kernel`
1097 The reflection of the kernel that was used to preconvolve the
1098 `science` exposure. Must be normalized to sum to 1.
1102 results : `lsst.pipe.base.Struct`
1104 ``scoreExposure`` : `lsst.afw.image.ExposureF`
1105 Result of subtracting the convolved template and science
1106 images. Attached PSF is that of the original science image.
1107 ``matchedTemplate`` : `lsst.afw.image.ExposureF`
1108 Warped and PSF-matched template exposure. Attached PSF is that
1109 of the original science image.
1110 ``matchedScience`` : `lsst.afw.image.ExposureF`
1111 The science exposure after convolving with its own PSF.
1112 Attached PSF is that of the original science image.
1113 ``backgroundModel`` : `lsst.afw.math.Function2D`
1114 Background model that was fit while solving for the
1116 ``psfMatchingKernel`` : `lsst.afw.math.Kernel`
1117 Final kernel used to PSF-match the template to the science
1120 bbox = science.getBBox()
1121 innerBBox = preConvKernel.shrinkBBox(bbox)
1123 kernelSources = self.makeKernel.selectKernelSources(template[innerBBox], matchedScience[innerBBox],
1124 candidateList=selectSources,
1126 kernelResult = self.makeKernel.run(template[innerBBox], matchedScience[innerBBox], kernelSources,
1129 matchedTemplate = self.
_convolveExposure(template, kernelResult.psfMatchingKernel,
1133 interpolateBadMaskPlanes=
True,
1134 photoCalib=science.photoCalib)
1136 backgroundModel=(kernelResult.backgroundModel
1137 if self.config.doSubtractBackground
else None))
1138 correctedScore = self.
finalize(template[bbox], science, score,
1139 kernelResult.psfMatchingKernel,
1140 templateMatched=
True, preConvMode=
True,
1141 preConvKernel=preConvKernel)
1143 return lsst.pipe.base.Struct(scoreExposure=correctedScore,
1144 matchedTemplate=matchedTemplate,
1145 matchedScience=matchedScience,
1146 backgroundModel=kernelResult.backgroundModel,
1147 psfMatchingKernel=kernelResult.psfMatchingKernel,
1148 kernelSources=kernelSources)
1152 exceptionMessage=""):
1153 """Raise NoWorkFound if template coverage < requiredTemplateFraction
1157 templateExposure : `lsst.afw.image.ExposureF`
1158 The template exposure to check
1159 logger : `logging.Logger`
1160 Logger for printing output.
1161 requiredTemplateFraction : `float`, optional
1162 Fraction of pixels of the science image required to have coverage
1164 exceptionMessage : `str`, optional
1165 Message to include in the exception raised if the template coverage
1170 templateCoverageFraction: `float`
1171 Fraction of pixels in the template with data.
1175 lsst.pipe.base.NoWorkFound
1176 Raised if fraction of good pixels, defined as not having NO_DATA
1177 set, is less than the requiredTemplateFraction
1181 pixNoData = np.count_nonzero(templateExposure.mask.array
1182 & templateExposure.mask.getPlaneBitMask(
'NO_DATA'))
1183 pixGood = templateExposure.getBBox().getArea() - pixNoData
1184 templateCoverageFraction = pixGood/templateExposure.getBBox().getArea()
1185 logger.info(
"template has %d good pixels (%.1f%%)", pixGood, 100*templateCoverageFraction)
1187 if templateCoverageFraction < requiredTemplateFraction:
1188 message = (
"Insufficient Template Coverage. (%.1f%% < %.1f%%)" % (
1189 100*templateCoverageFraction,
1190 100*requiredTemplateFraction))
1191 raise lsst.pipe.base.NoWorkFound(message +
" " + exceptionMessage)
1192 return templateCoverageFraction
1196 """Subtract template from science, propagating relevant metadata.
1200 science : `lsst.afw.Exposure`
1201 The input science image.
1202 template : `lsst.afw.Exposure`
1203 The template to subtract from the science image.
1204 backgroundModel : `lsst.afw.MaskedImage`, optional
1205 Differential background model
1209 difference : `lsst.afw.Exposure`
1210 The subtracted image.
1212 difference = science.clone()
1213 if backgroundModel
is not None:
1214 difference.maskedImage -= backgroundModel
1215 difference.maskedImage -= template.maskedImage
1220 """Determine that the PSF of ``exp1`` is not wider than that of ``exp2``.
1224 exp1 : `~lsst.afw.image.Exposure`
1225 Exposure with the reference point spread function (PSF) to evaluate.
1226 exp2 : `~lsst.afw.image.Exposure`
1227 Exposure with a candidate point spread function (PSF) to evaluate.
1228 fwhmExposureBuffer : `float`
1229 Fractional buffer margin to be left out of all sides of the image
1230 during the construction of the grid to compute mean PSF FWHM in an
1231 exposure, if the PSF is not available at its average position.
1232 fwhmExposureGrid : `int`
1233 Grid size to compute the mean FWHM in an exposure, if the PSF is not
1234 available at its average position.
1238 True if ``exp1`` has a PSF that is not wider than that of ``exp2`` in
1242 shape1 = getPsfFwhm(exp1.psf, average=
False)
1243 shape2 = getPsfFwhm(exp2.psf, average=
False)
1245 shape1 = evaluateMeanPsfFwhm(exp1,
1246 fwhmExposureBuffer=fwhmExposureBuffer,
1247 fwhmExposureGrid=fwhmExposureGrid
1249 shape2 = evaluateMeanPsfFwhm(exp2,
1250 fwhmExposureBuffer=fwhmExposureBuffer,
1251 fwhmExposureGrid=fwhmExposureGrid
1253 return shape1 <= shape2
1256 xTest = shape1[0] <= shape2[0]
1257 yTest = shape1[1] <= shape2[1]
1258 return xTest | yTest
1262 """Replace masked image pixels with interpolated values.
1266 maskedImage : `lsst.afw.image.MaskedImage`
1267 Image on which to perform interpolation.
1268 badMaskPlanes : `list` of `str`
1269 List of mask planes to interpolate over.
1270 fallbackValue : `float`, optional
1271 Value to set when interpolation fails.
1276 The number of masked pixels that were replaced.
1278 imgBadMaskPlanes = [
1279 maskPlane
for maskPlane
in badMaskPlanes
if maskPlane
in maskedImage.mask.getMaskPlaneDict()
1282 image = maskedImage.image.array
1283 badPixels = (maskedImage.mask.array & maskedImage.mask.getPlaneBitMask(imgBadMaskPlanes)) > 0
1284 image[badPixels] = np.nan
1285 if fallbackValue
is None:
1286 fallbackValue = np.nanmedian(image)
1289 image[badPixels] = fallbackValue
1290 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)