3232from lsst .ip .diffim .utils import (evaluateMaskFraction , computeDifferenceImageMetrics ,
3333 populate_sattle_visit_cache )
3434from lsst .meas .algorithms import SkyObjectsTask , SourceDetectionTask , SetPrimaryFlagsTask , MaskStreaksTask
35- from lsst .meas .algorithms import FindGlintTrailsTask
35+ from lsst .meas .algorithms import FindGlintTrailsTask , FindCosmicRaysConfig , findCosmicRays
3636from lsst .meas .base import ForcedMeasurementTask , ApplyApCorrTask , DetectorVisitIdGeneratorConfig
3737import lsst .meas .deblender
3838import lsst .meas .extensions .trailedSources # noqa: F401
3939import lsst .meas .extensions .shapeHSM
4040import lsst .pex .config as pexConfig
41- from lsst .pex .exceptions import InvalidParameterError
41+ from lsst .pex .exceptions import InvalidParameterError , LengthError
4242import lsst .pipe .base as pipeBase
4343import lsst .utils
4444from lsst .utils .timer import timeMethod
@@ -81,6 +81,33 @@ def metadata(self):
8181 return {}
8282
8383
84+ class TooManyCosmicRays (pipeBase .AlgorithmError ):
85+ """Raised if the cosmic ray task fails with too many cosmics.
86+
87+ Parameters
88+ ----------
89+ maxCosmicRays : `int`
90+ Maximum number of cosmic rays allowed.
91+ """
92+ def __init__ (self , maxCosmicRays , ** kwargs ):
93+ msg = f"Cosmic ray task found more than { maxCosmicRays } cosmics."
94+ self .msg = msg
95+ self ._metadata = kwargs
96+ super ().__init__ (msg , ** kwargs )
97+ self ._metadata ["maxCosmicRays" ] = maxCosmicRays
98+
99+ def __str__ (self ):
100+ # Exception doesn't handle **kwargs, so we need a custom str.
101+ return f"{ self .msg } : { self .metadata } "
102+
103+ @property
104+ def metadata (self ):
105+ for key , value in self ._metadata .items ():
106+ if not (isinstance (value , int ) or isinstance (value , float ) or isinstance (value , str )):
107+ raise TypeError (f"{ key } is of type { type (value )} , but only (int, float, str) are allowed." )
108+ return self ._metadata
109+
110+
84111class DetectAndMeasureConnections (pipeBase .PipelineTaskConnections ,
85112 dimensions = ("instrument" , "visit" , "detector" ),
86113 defaultTemplates = {"coaddName" : "deep" ,
@@ -202,6 +229,16 @@ class DetectAndMeasureConfig(pipeBase.PipelineTaskConfig,
202229 target = lsst .meas .algorithms .SubtractBackgroundTask ,
203230 doc = "Task to perform final background subtraction, after first detection pass." ,
204231 )
232+ doFindCosmicRays = pexConfig .Field (
233+ dtype = bool ,
234+ doc = "Detect and mask cosmic rays on the difference image?"
235+ "CRs can be interpolated over by setting cosmicray.keepCRs=False" ,
236+ default = True ,
237+ )
238+ cosmicray = pexConfig .ConfigField (
239+ dtype = FindCosmicRaysConfig ,
240+ doc = "Options for finding and masking cosmic rays" ,
241+ )
205242 detection = pexConfig .ConfigurableField (
206243 target = SourceDetectionTask ,
207244 doc = "Final source detection for diaSource measurement" ,
@@ -377,6 +414,8 @@ def setDefaults(self):
377414 "DETECTED_NEGATIVE" ,
378415 "NO_DATA" ,
379416 ]
417+ # Cosmic ray detection
418+ self .cosmicray .keepCRs = True # do not interpolate over detected CRs
380419 # DiaSource Detection
381420 self .detection .thresholdPolarity = "both"
382421 self .detection .thresholdValue = 5.0
@@ -621,6 +660,11 @@ def run(self, science, matchedTemplate, difference, kernelSources=None,
621660
622661 self ._prepareInputs (detectionExposure )
623662
663+ if self .config .doFindCosmicRays and not self .config .doSubtractBackground :
664+ # Detect and interpolate over any remaining cosmic rays
665+ # This only needs to run once, right before the final detection.
666+ self .findAndMaskCosmicRays (detectionExposure )
667+
624668 # Don't use the idFactory until after deblend+merge, so that we aren't
625669 # generating ids that just get thrown away (footprint merge doesn't
626670 # know about past ids).
@@ -639,6 +683,10 @@ def run(self, science, matchedTemplate, difference, kernelSources=None,
639683 difference .setMask (detectionExposure .mask )
640684 background = self .subtractFinalBackground .run (difference ).background
641685
686+ if self .config .doFindCosmicRays :
687+ # Detect and interpolate over any remaining cosmic rays
688+ self .findAndMaskCosmicRays (difference )
689+
642690 # Re-run detection to get final footprints
643691 table = afwTable .SourceTable .make (self .schema )
644692 results = self .detection .run (
@@ -1049,6 +1097,39 @@ def measureForcedSources(self, diaSources, image, wcs, template=False):
10491097 for diaSource , forcedSource in zip (diaSources , forcedSources ):
10501098 diaSource .assign (forcedSource , mapper )
10511099
1100+ def findAndMaskCosmicRays (self , difference ):
1101+ """Detect and mask cosmic rays on the difference image.
1102+
1103+ Parameters
1104+ ----------
1105+ difference : `lsst.afw.image.Exposure`
1106+ The background-subtracted difference image.
1107+ The mask plane will be modified in place.
1108+ """
1109+ # This is run on a background-subtracted difference image, so the
1110+ # remaining background should be ~0.
1111+ medianBg = 0.
1112+ try :
1113+ crs = findCosmicRays (
1114+ difference .maskedImage ,
1115+ difference .psf ,
1116+ medianBg ,
1117+ pexConfig .makePropertySet (self .config .cosmicray ),
1118+ self .config .cosmicray .keepCRs ,
1119+ )
1120+ except LengthError :
1121+ raise TooManyCosmicRays (self .config .cosmicray .nCrPixelMax ) from None
1122+ num = 0
1123+ if crs is not None :
1124+ mask = difference .maskedImage .mask
1125+ crBit = mask .getPlaneBitMask ("CR" )
1126+ afwDetection .setMaskFromFootprintList (mask , crs , crBit )
1127+ num = len (crs )
1128+
1129+ text = "masked" if self .config .cosmicray .keepCRs else "interpolated over"
1130+ self .log .info ("Identified and %s %s cosmic rays." , text , num )
1131+ self .metadata ["cosmic_ray_count" ] = num
1132+
10521133 def calculateMetrics (self , science , difference , diaSources , kernelSources ):
10531134 """Add difference image QA metrics to the Task metadata.
10541135
0 commit comments