33 populate_sattle_visit_cache)
34from lsst.meas.algorithms
import SkyObjectsTask, SourceDetectionTask, SetPrimaryFlagsTask, MaskStreaksTask
35from lsst.meas.algorithms
import FindGlintTrailsTask, FindCosmicRaysConfig, findCosmicRays
36from lsst.meas.base import ForcedMeasurementTask, ApplyApCorrTask, DetectorVisitIdGeneratorConfig
37import lsst.meas.deblender
38import lsst.meas.extensions.trailedSources
39import lsst.meas.extensions.shapeHSM
44from lsst.utils.timer
import timeMethod
46from .
import DipoleFitTask
48__all__ = [
"DetectAndMeasureConfig",
"DetectAndMeasureTask",
49 "DetectAndMeasureScoreConfig",
"DetectAndMeasureScoreTask"]
53 """Raised when the residuals in footprints of stars used to compute the
54 psf-matching kernel exceeds the configured maximum.
57 msg = (
"The ratio of residual power in source footprints on the"
58 " difference image to the power in the footprints on the"
59 f
" science image was {ratio}, which exceeds the maximum"
60 f
" threshold of {threshold}")
67 return {
"ratio": self.
ratio,
73 """Raised when there are no diaSources detected on an image difference.
76 msg = (
"No diaSources detected!")
85 """Raised if the cosmic ray task fails with too many cosmics.
90 Maximum number of cosmic rays allowed.
93 msg = f
"Cosmic ray task found more than {maxCosmicRays} cosmics."
97 self.
_metadata[
"maxCosmicRays"] = maxCosmicRays
101 return f
"{self.msg}: {self.metadata}"
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.")
112 dimensions=(
"instrument",
"visit",
"detector"),
113 defaultTemplates={
"coaddName":
"deep",
114 "warpTypeSuffix":
"",
116 science = pipeBase.connectionTypes.Input(
117 doc=
"Input science exposure.",
118 dimensions=(
"instrument",
"visit",
"detector"),
119 storageClass=
"ExposureF",
120 name=
"{fakesType}calexp"
122 matchedTemplate = pipeBase.connectionTypes.Input(
123 doc=
"Warped and PSF-matched template used to create the difference image.",
124 dimensions=(
"instrument",
"visit",
"detector"),
125 storageClass=
"ExposureF",
126 name=
"{fakesType}{coaddName}Diff_matchedExp",
128 difference = pipeBase.connectionTypes.Input(
129 doc=
"Result of subtracting template from science.",
130 dimensions=(
"instrument",
"visit",
"detector"),
131 storageClass=
"ExposureF",
132 name=
"{fakesType}{coaddName}Diff_differenceTempExp",
134 kernelSources = pipeBase.connectionTypes.Input(
135 doc=
"Final selection of sources used for psf matching.",
136 dimensions=(
"instrument",
"visit",
"detector"),
137 storageClass=
"SourceCatalog",
138 name=
"{fakesType}{coaddName}Diff_psfMatchSources"
140 outputSchema = pipeBase.connectionTypes.InitOutput(
141 doc=
"Schema (as an example catalog) for output DIASource catalog.",
142 storageClass=
"SourceCatalog",
143 name=
"{fakesType}{coaddName}Diff_diaSrc_schema",
145 diaSources = pipeBase.connectionTypes.Output(
146 doc=
"Detected diaSources on the difference image.",
147 dimensions=(
"instrument",
"visit",
"detector"),
148 storageClass=
"SourceCatalog",
149 name=
"{fakesType}{coaddName}Diff_diaSrc",
151 subtractedMeasuredExposure = pipeBase.connectionTypes.Output(
152 doc=
"Difference image with detection mask plane filled in.",
153 dimensions=(
"instrument",
"visit",
"detector"),
154 storageClass=
"ExposureF",
155 name=
"{fakesType}{coaddName}Diff_differenceExp",
157 differenceBackground = pipeBase.connectionTypes.Output(
158 doc=
"Background model that was subtracted from the difference image.",
159 dimensions=(
"instrument",
"visit",
"detector"),
160 storageClass=
"Background",
161 name=
"difference_background",
163 maskedStreaks = pipeBase.connectionTypes.Output(
164 doc=
'Catalog of streak fit parameters for the difference image.',
165 storageClass=
"ArrowNumpyDict",
166 dimensions=(
"instrument",
"visit",
"detector"),
167 name=
"{fakesType}{coaddName}Diff_streaks",
169 glintTrailInfo = pipeBase.connectionTypes.Output(
170 doc=
'Dict of fit parameters for glint trails in the catalog.',
171 storageClass=
"ArrowNumpyDict",
172 dimensions=(
"instrument",
"visit",
"detector"),
173 name=
"trailed_glints",
176 def __init__(self, *, config):
177 super().__init__(config=config)
178 if not (self.config.doCalculateResidualMetics):
179 self.inputs.remove(
"kernelSources")
180 if not (self.config.writeStreakInfo
and self.config.doMaskStreaks):
181 self.outputs.remove(
"maskedStreaks")
182 if not (self.config.doSubtractBackground
and self.config.doWriteBackground):
183 self.outputs.remove(
"differenceBackground")
184 if not (self.config.writeGlintInfo):
185 self.outputs.remove(
"glintTrailInfo")
188class DetectAndMeasureConfig(pipeBase.PipelineTaskConfig,
189 pipelineConnections=DetectAndMeasureConnections):
190 """Config for DetectAndMeasureTask
192 doMerge = pexConfig.Field(
195 doc=
"Merge positive and negative diaSources with grow radius "
196 "set by growFootprint"
198 doForcedMeasurement = pexConfig.Field(
201 doc=
"Force photometer diaSource locations on PVI?"
203 doAddMetrics = pexConfig.Field(
206 doc=
"Add columns to the source table to hold analysis metrics?"
208 doSubtractBackground = pexConfig.Field(
210 doc=
"Subtract a background model from the image before detection?",
213 doWriteBackground = pexConfig.Field(
215 doc=
"Persist the fitted background model?",
218 doCalculateResidualMetics = pexConfig.Field(
220 doc=
"Calculate metrics to assess image subtraction quality for the task"
224 subtractInitialBackground = pexConfig.ConfigurableField(
225 target=lsst.meas.algorithms.SubtractBackgroundTask,
226 doc=
"Task to perform intial background subtraction, before first detection pass.",
228 subtractFinalBackground = pexConfig.ConfigurableField(
229 target=lsst.meas.algorithms.SubtractBackgroundTask,
230 doc=
"Task to perform final background subtraction, after first detection pass.",
232 doFindCosmicRays = pexConfig.Field(
234 doc=
"Detect and mask cosmic rays on the difference image?"
235 "CRs can be interpolated over by setting cosmicray.keepCRs=False",
238 cosmicray = pexConfig.ConfigField(
239 dtype=FindCosmicRaysConfig,
240 doc=
"Options for finding and masking cosmic rays",
242 detection = pexConfig.ConfigurableField(
243 target=SourceDetectionTask,
244 doc=
"Final source detection for diaSource measurement",
246 streakDetection = pexConfig.ConfigurableField(
247 target=SourceDetectionTask,
248 doc=
"Separate source detection used only for streak masking",
250 doDeblend = pexConfig.Field(
253 doc=
"Deblend DIASources after detection?"
255 deblend = pexConfig.ConfigurableField(
256 target=lsst.meas.deblender.SourceDeblendTask,
257 doc=
"Task to split blended sources into their components."
259 measurement = pexConfig.ConfigurableField(
260 target=DipoleFitTask,
261 doc=
"Task to measure sources on the difference image.",
263 doApCorr = lsst.pex.config.Field(
266 doc=
"Run subtask to apply aperture corrections"
268 applyApCorr = lsst.pex.config.ConfigurableField(
269 target=ApplyApCorrTask,
270 doc=
"Task to apply aperture corrections"
272 forcedMeasurement = pexConfig.ConfigurableField(
273 target=ForcedMeasurementTask,
274 doc=
"Task to force photometer science image at diaSource locations.",
276 growFootprint = pexConfig.Field(
279 doc=
"Grow positive and negative footprints by this many pixels before merging"
281 diaSourceMatchRadius = pexConfig.Field(
284 doc=
"Match radius (in arcseconds) for DiaSource to Source association"
286 doSkySources = pexConfig.Field(
289 doc=
"Generate sky sources?",
291 skySources = pexConfig.ConfigurableField(
292 target=SkyObjectsTask,
293 doc=
"Generate sky sources",
295 doMaskStreaks = pexConfig.Field(
298 doc=
"Turn on streak masking",
300 maskStreaks = pexConfig.ConfigurableField(
301 target=MaskStreaksTask,
302 doc=
"Subtask for masking streaks. Only used if doMaskStreaks is True. "
303 "Adds a mask plane to an exposure, with the mask plane name set by streakMaskName.",
305 streakBinFactor = pexConfig.Field(
308 doc=
"Bin scale factor to use when rerunning detection for masking streaks. "
309 "Only used if doMaskStreaks is True.",
311 writeStreakInfo = pexConfig.Field(
314 doc=
"Record the parameters of any detected streaks. For LSST, this should be turned off except for "
317 findGlints = pexConfig.ConfigurableField(
318 target=FindGlintTrailsTask,
319 doc=
"Subtask for finding glint trails, usually caused by satellites or debris."
321 writeGlintInfo = pexConfig.Field(
324 doc=
"Record the parameters of any detected glint trails."
326 setPrimaryFlags = pexConfig.ConfigurableField(
327 target=SetPrimaryFlagsTask,
328 doc=
"Task to add isPrimary and deblending-related flags to the catalog."
330 badSourceFlags = lsst.pex.config.ListField(
332 doc=
"Sources with any of these flags set are removed before writing the output catalog.",
333 default=(
"base_PixelFlags_flag_offimage",
334 "base_PixelFlags_flag_interpolatedCenterAll",
335 "base_PixelFlags_flag_badCenter",
336 "base_PixelFlags_flag_edgeCenter",
337 "base_PixelFlags_flag_nodataCenter",
338 "base_PixelFlags_flag_saturatedCenter",
339 "base_PixelFlags_flag_sat_templateCenter",
340 "base_PixelFlags_flag_spikeCenter",
343 clearMaskPlanes = lsst.pex.config.ListField(
345 doc=
"Mask planes to clear before running detection.",
346 default=(
"DETECTED",
"DETECTED_NEGATIVE",
"NOT_DEBLENDED",
"STREAK"),
348 doRejectBadMaskPlaneDetections = pexConfig.Field(
351 doc=
"Reject any peaks detected on ``badMaskPlanes`` before measurement."
352 "These should all be rejected downstream by ``badSourceFlags``, "
353 "but filtering earlier can save time."
355 badMaskPlanes = lsst.pex.config.ListField(
357 doc=
"Detections whose footprint peak lies on a pixel with any of these"
358 " mask planes set will be rejected before measurement."
359 " Any missing mask planes will be silently ignored.",
360 default=(
"NO_DATA",
"BAD",
"SAT",
"EDGE"),
362 raiseOnBadSubtractionRatio = pexConfig.Field(
365 doc=
"Raise an error if the ratio of power in detected footprints"
366 " on the difference image to the power in footprints on the science"
367 " image exceeds ``badSubtractionRatioThreshold``",
369 badSubtractionRatioThreshold = pexConfig.Field(
372 doc=
"Maximum ratio of power in footprints on the difference image to"
373 " the same footprints on the science image."
374 "Only used if ``raiseOnBadSubtractionRatio`` is set",
376 badSubtractionVariationThreshold = pexConfig.Field(
379 doc=
"Maximum standard deviation of the ratio of power in footprints on"
380 " the difference image to the same footprints on the science image."
381 "Only used if ``raiseOnBadSubtractionRatio`` is set",
383 raiseOnNoDiaSources = pexConfig.Field(
386 doc=
"Raise an algorithm error if no diaSources are detected.",
388 run_sattle = pexConfig.Field(
391 doc=
"If true, dia source bounding boxes will be sent for verification"
392 "to the sattle service."
394 sattle_historical = pexConfig.Field(
397 doc=
"If re-running a pipeline that requires sattle, this should be set "
398 "to True. This will populate sattle's cache with the historic data "
399 "closest in time to the exposure."
401 idGenerator = DetectorVisitIdGeneratorConfig.make_field()
403 def setDefaults(self):
408 self.subtractInitialBackground.binSize = 8
409 self.subtractInitialBackground.useApprox =
False
410 self.subtractInitialBackground.statisticsProperty =
"MEDIAN"
411 self.subtractInitialBackground.doFilterSuperPixels =
True
412 self.subtractInitialBackground.ignoredPixelMask = [
"BAD",
420 self.subtractFinalBackground.binSize = 40
421 self.subtractFinalBackground.useApprox =
False
422 self.subtractFinalBackground.statisticsProperty =
"MEDIAN"
423 self.subtractFinalBackground.doFilterSuperPixels =
True
424 self.subtractFinalBackground.ignoredPixelMask = [
"BAD",
431 self.cosmicray.keepCRs =
True
433 self.detection.thresholdPolarity =
"both"
434 self.detection.thresholdValue = 5.0
435 self.detection.reEstimateBackground =
False
436 self.detection.thresholdType =
"pixel_stdev"
437 self.detection.excludeMaskPlanes = []
440 self.streakDetection.thresholdType = self.detection.thresholdType
441 self.streakDetection.reEstimateBackground =
False
442 self.streakDetection.excludeMaskPlanes = self.detection.excludeMaskPlanes
443 self.streakDetection.thresholdValue = self.detection.thresholdValue
445 self.streakDetection.thresholdPolarity =
"positive"
447 self.streakDetection.nSigmaToGrow = 0
450 self.maskStreaks.onlyMaskDetected =
False
452 self.maskStreaks.maxStreakWidth = 100
455 self.maskStreaks.maxFitIter = 10
457 self.maskStreaks.nSigmaMask = 2
460 self.maskStreaks.absMinimumKernelHeight = 2
462 self.measurement.plugins.names |= [
"ext_trailedSources_Naive",
463 "base_LocalPhotoCalib",
465 "ext_shapeHSM_HsmSourceMoments",
466 "ext_shapeHSM_HsmPsfMoments",
467 "base_ClassificationSizeExtendedness",
469 self.measurement.slots.psfShape =
"ext_shapeHSM_HsmPsfMoments"
470 self.measurement.slots.shape =
"ext_shapeHSM_HsmSourceMoments"
471 self.measurement.plugins[
"base_SdssCentroid"].maxDistToPeak = 5.0
472 self.forcedMeasurement.plugins = [
"base_TransformedCentroid",
"base_PsfFlux"]
473 self.forcedMeasurement.copyColumns = {
474 "id":
"objectId",
"parent":
"parentObjectId",
"coord_ra":
"coord_ra",
"coord_dec":
"coord_dec"}
475 self.forcedMeasurement.slots.centroid =
"base_TransformedCentroid"
476 self.forcedMeasurement.slots.shape =
None
479 self.measurement.plugins[
"base_PixelFlags"].masksFpAnywhere = [
480 "STREAK",
"INJECTED",
"INJECTED_TEMPLATE",
"HIGH_VARIANCE",
"SAT_TEMPLATE",
"SPIKE"]
481 self.measurement.plugins[
"base_PixelFlags"].masksFpCenter = [
482 "STREAK",
"INJECTED",
"INJECTED_TEMPLATE",
"HIGH_VARIANCE",
"SAT_TEMPLATE",
"SPIKE"]
483 self.skySources.avoidMask = [
"DETECTED",
"DETECTED_NEGATIVE",
"BAD",
"NO_DATA",
"EDGE"]
489 if not os.getenv(
"SATTLE_URI_BASE"):
490 raise pexConfig.FieldValidationError(DetectAndMeasureConfig.run_sattle, self,
491 "Sattle requested but SATTLE_URI_BASE "
492 "environment variable not set.")
495class DetectAndMeasureTask(lsst.pipe.base.PipelineTask):
496 """Detect and measure sources on a difference image.
498 ConfigClass = DetectAndMeasureConfig
499 _DefaultName =
"detectAndMeasure"
501 def __init__(self, **kwargs):
502 super().__init__(**kwargs)
503 self.schema = afwTable.SourceTable.makeMinimalSchema()
506 if self.config.doSubtractBackground:
507 self.makeSubtask(
"subtractInitialBackground")
508 self.makeSubtask(
"subtractFinalBackground")
509 self.makeSubtask(
"detection", schema=self.schema)
510 if self.config.doDeblend:
511 self.makeSubtask(
"deblend", schema=self.schema)
512 self.makeSubtask(
"setPrimaryFlags", schema=self.schema, isSingleFrame=
True)
513 self.makeSubtask(
"measurement", schema=self.schema,
514 algMetadata=self.algMetadata)
515 if self.config.doApCorr:
516 self.makeSubtask(
"applyApCorr", schema=self.measurement.schema)
517 if self.config.doForcedMeasurement:
518 self.schema.addField(
519 "ip_diffim_forced_PsfFlux_instFlux",
"D",
520 "Forced PSF flux measured on the direct image.",
522 self.schema.addField(
523 "ip_diffim_forced_PsfFlux_instFluxErr",
"D",
524 "Forced PSF flux error measured on the direct image.",
526 self.schema.addField(
527 "ip_diffim_forced_PsfFlux_area",
"F",
528 "Forced PSF flux effective area of PSF.",
530 self.schema.addField(
531 "ip_diffim_forced_PsfFlux_flag",
"Flag",
532 "Forced PSF flux general failure flag.")
533 self.schema.addField(
534 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
"Flag",
535 "Forced PSF flux not enough non-rejected pixels in data to attempt the fit.")
536 self.schema.addField(
537 "ip_diffim_forced_PsfFlux_flag_edge",
"Flag",
538 "Forced PSF flux object was too close to the edge of the image to use the full PSF model.")
539 self.schema.addField(
540 "ip_diffim_forced_template_PsfFlux_instFlux",
"D",
541 "Forced PSF flux measured on the template image.",
543 self.schema.addField(
544 "ip_diffim_forced_template_PsfFlux_instFluxErr",
"D",
545 "Forced PSF flux error measured on the template image.",
547 self.schema.addField(
548 "ip_diffim_forced_template_PsfFlux_area",
"F",
549 "Forced template PSF flux effective area of PSF.",
551 self.schema.addField(
552 "ip_diffim_forced_template_PsfFlux_flag",
"Flag",
553 "Forced template PSF flux general failure flag.")
554 self.schema.addField(
555 "ip_diffim_forced_template_PsfFlux_flag_noGoodPixels",
"Flag",
556 "Forced template PSF flux not enough non-rejected pixels in data to attempt the fit.")
557 self.schema.addField(
558 "ip_diffim_forced_template_PsfFlux_flag_edge",
"Flag",
559 """Forced template PSF flux object was too close to the edge of the image """
560 """to use the full PSF model.""")
561 self.makeSubtask(
"forcedMeasurement", refSchema=self.schema)
563 self.schema.addField(
"refMatchId",
"L",
"unique id of reference catalog match")
564 self.schema.addField(
"srcMatchId",
"L",
"unique id of source match")
567 self.makeSubtask(
"skySources", schema=self.schema)
568 if self.config.doMaskStreaks:
569 self.makeSubtask(
"maskStreaks")
570 self.makeSubtask(
"streakDetection")
571 self.makeSubtask(
"findGlints")
572 self.schema.addField(
"glint_trail",
"Flag",
"DiaSource is part of a glint trail.")
573 self.schema.addField(
"reliability", type=
"F", doc=
"Reliability score of the DiaSource")
574 self.schema.addField(
"reliabilityVersion", type=str, size=7, doc=
"Version of the reliability model")
581 for flag
in self.config.badSourceFlags:
582 if flag
not in self.schema:
583 raise pipeBase.InvalidQuantumError(
"Field %s not in schema" % flag)
586 self.outputSchema = afwTable.SourceCatalog(self.schema)
587 self.outputSchema.getTable().setMetadata(self.algMetadata)
589 def runQuantum(self, butlerQC: pipeBase.QuantumContext,
590 inputRefs: pipeBase.InputQuantizedConnection,
591 outputRefs: pipeBase.OutputQuantizedConnection):
592 inputs = butlerQC.get(inputRefs)
593 idGenerator = self.config.idGenerator.apply(butlerQC.quantum.dataId)
594 idFactory = idGenerator.make_table_id_factory()
597 measurementResults = pipeBase.Struct(
598 subtractedMeasuredExposure=
None,
601 differenceBackground=
None,
604 self.run(**inputs, idFactory=idFactory, measurementResults=measurementResults)
605 except pipeBase.AlgorithmError
as e:
606 error = pipeBase.AnnotatedPartialOutputsError.annotate(
609 measurementResults.subtractedMeasuredExposure,
610 measurementResults.diaSources,
611 measurementResults.maskedStreaks,
614 butlerQC.put(measurementResults, outputRefs)
616 butlerQC.put(measurementResults, outputRefs)
619 def run(self, science, matchedTemplate, difference, kernelSources=None,
620 idFactory=None, measurementResults=None):
621 """Detect and measure sources on a difference image.
623 The difference image will be convolved with a gaussian approximation of
624 the PSF to form a maximum likelihood image for detection.
625 Close positive and negative detections will optionally be merged into
627 Sky sources, or forced detections in background regions, will optionally
628 be added, and the configured measurement algorithm will be run on all
633 science : `lsst.afw.image.ExposureF`
634 Science exposure that the template was subtracted from.
635 matchedTemplate : `lsst.afw.image.ExposureF`
636 Warped and PSF-matched template that was used produce the
638 difference : `lsst.afw.image.ExposureF`
639 Result of subtracting template from the science image.
640 kernelSources : `lsst.afw.table.SourceCatalog`, optional
641 Final selection of sources that was used for psf matching.
642 idFactory : `lsst.afw.table.IdFactory`, optional
643 Generator object used to assign ids to detected sources in the
644 difference image. Ids from this generator are not set until after
645 deblending and merging positive/negative peaks.
646 measurementResults : `lsst.pipe.base.Struct`, optional
647 Result struct that is modified to allow saving of partial outputs
648 for some failure conditions. If the task completes successfully,
649 this is also returned.
653 measurementResults : `lsst.pipe.base.Struct`
655 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
656 Subtracted exposure with detection mask applied.
657 ``diaSources`` : `lsst.afw.table.SourceCatalog`
658 The catalog of detected sources.
659 ``differenceBackground`` : `lsst.afw.math.BackgroundList`
660 Background that was subtracted from the difference image.
662 if measurementResults
is None:
663 measurementResults = pipeBase.Struct()
664 if idFactory
is None:
665 idFactory = lsst.meas.base.IdGenerator().make_table_id_factory()
668 self._prepareInputs(difference)
669 if self.config.doSubtractBackground:
670 background = self._fitAndSubtractBackground(differenceExposure=difference)
672 background = afwMath.BackgroundList()
674 if self.config.doFindCosmicRays:
675 self.findAndMaskCosmicRays(difference)
680 table = afwTable.SourceTable.make(self.schema)
681 results = self.detection.
run(
685 background=background,
688 measurementResults.differenceBackground = background
690 if self.config.doDeblend:
691 sources, positives, negatives = self._deblend(difference,
696 positives = afwTable.SourceCatalog(self.schema)
697 results.positive.makeSources(positives)
698 negatives = afwTable.SourceCatalog(self.schema)
699 results.negative.makeSources(negatives)
700 sources = results.sources
702 self.processResults(science, matchedTemplate, difference,
703 sources, idFactory, kernelSources,
706 measurementResults=measurementResults)
707 return measurementResults
709 def _prepareInputs(self, difference):
710 """Ensure that we start with an empty detection and deblended mask.
714 difference : `lsst.afw.image.ExposureF`
715 The difference image that will be used for detecting diaSources.
716 The mask plane will be modified in place.
720 lsst.pipe.base.UpstreamFailureNoWorkFound
721 If the PSF is not usable for measurement.
724 sigma = difference.psf.computeShape(difference.psf.getAveragePosition()).getDeterminantRadius()
726 raise pipeBase.UpstreamFailureNoWorkFound(
"Invalid PSF detected! PSF width evaluates to NaN.")
728 mask = difference.mask
729 for mp
in self.config.clearMaskPlanes:
730 if mp
not in mask.getMaskPlaneDict():
731 mask.addMaskPlane(mp)
732 mask &= ~mask.getPlaneBitMask(self.config.clearMaskPlanes)
734 def _fitAndSubtractBackground(self, differenceExposure, scoreExposure=None):
735 """Fit the final background to ``differenceExposure``.
737 A rough background is first fit on the difference and an internal
738 first-pass detection is run on the (background-subtracted) clone; the
739 resulting detection mask is then transferred to ``differenceExposure``
740 so the final background fit can mask out real sources. This two-pass
741 scheme is what suppresses spurious detections from glints and
742 extended structures: the small-binsize initial fit aggressively
743 over-subtracts those features, and the resulting peak mask lets the
744 large-binsize final fit produce a clean background. When
745 ``scoreExposure`` is supplied (score-image variant), the final
746 background is also subtracted from its image so the caller's final
747 detection runs on a properly background-subtracted score.
749 The first-pass detection's `Struct` is discarded; only the mask it
750 leaves on the clone is used downstream.
754 differenceExposure : `lsst.afw.image.ExposureF`
755 Difference image. The final background is subtracted from its
756 image in place but its mask is not modified.
757 scoreExposure : `lsst.afw.image.ExposureF`, optional
758 Maximum-likelihood score image. When supplied, the final
759 background is also subtracted from its image.
763 background : `lsst.afw.math.BackgroundList`
764 The fit final background.
766 if scoreExposure
is None:
767 detectionExposure = differenceExposure.clone()
768 background = self.subtractInitialBackground.
run(detectionExposure).background
773 background = self.subtractInitialBackground.
run(differenceExposure.clone()).background
774 detectionExposure = scoreExposure.clone()
775 detectionExposure.image -= background.getImage()
777 table = afwTable.SourceTable.make(self.schema)
780 exposure=detectionExposure,
782 background=background,
788 detectedBit = differenceExposure.mask.getPlaneBitMask([
"DETECTED"])
789 detectedPix = detectionExposure.mask.array & detectedBit > 0
790 detectedNegativeBit = differenceExposure.mask.getPlaneBitMask([
"DETECTED_NEGATIVE"])
791 detectedNegativePix = detectionExposure.mask.array & detectedNegativeBit > 0
792 differenceExposure.mask.array[detectedPix] |= detectedBit
793 differenceExposure.mask.array[detectedNegativePix] |= detectedNegativeBit
794 background = self.subtractFinalBackground.
run(differenceExposure).background
795 if scoreExposure
is not None:
798 scoreExposure.image -= background.getImage()
801 def processResults(self, science, matchedTemplate, difference, sources, idFactory,
802 kernelSources=None, positives=None, negatives=None, measurementResults=None):
803 """Measure and process the results of source detection.
807 science : `lsst.afw.image.ExposureF`
808 Science exposure that the template was subtracted from.
809 matchedTemplate : `lsst.afw.image.ExposureF`
810 Warped and PSF-matched template that was used produce the
812 difference : `lsst.afw.image.ExposureF`
813 Result of subtracting template from the science image.
814 sources : `lsst.afw.table.SourceCatalog`
815 Detected sources on the difference exposure.
816 idFactory : `lsst.afw.table.IdFactory`
817 Generator object used to assign ids to detected sources in the
819 kernelSources : `lsst.afw.table.SourceCatalog`, optional
820 Final selection of sources that was used for psf matching.
821 positives : `lsst.afw.table.SourceCatalog`, optional
822 Positive polarity footprints.
823 negatives : `lsst.afw.table.SourceCatalog`, optional
824 Negative polarity footprints.
825 measurementResults : `lsst.pipe.base.Struct`, optional
826 Result struct that is modified to allow saving of partial outputs
827 for some failure conditions. If the task completes successfully,
828 this is also returned.
832 measurementResults : `lsst.pipe.base.Struct`
834 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
835 Subtracted exposure with detection mask applied.
836 ``diaSources`` : `lsst.afw.table.SourceCatalog`
837 The catalog of detected sources.
839 if measurementResults
is None:
840 measurementResults = pipeBase.Struct()
841 self.metadata[
"nUnmergedDiaSources"] = len(sources)
842 if self.config.doMerge:
844 if len(positives) > 0:
845 peakSchema = positives[0].getFootprint().peaks.schema
846 elif len(negatives) > 0:
847 peakSchema = negatives[0].getFootprint().peaks.schema
849 peakSchema = afwDetection.PeakTable.makeMinimalSchema()
850 mergeList = afwDetection.FootprintMergeList(self.schema,
851 [
"positive",
"negative"], peakSchema)
852 initialDiaSources = afwTable.SourceCatalog(self.schema)
856 mergeList.addCatalog(initialDiaSources.table, positives,
"positive", minNewPeakDist=0)
857 mergeList.addCatalog(initialDiaSources.table, negatives,
"negative", minNewPeakDist=0)
858 mergeList.getFinalSources(initialDiaSources)
861 initialDiaSources[
"is_negative"] = initialDiaSources[
"merge_footprint_negative"] & \
862 ~initialDiaSources[
"merge_footprint_positive"]
863 self.log.info(
"Merging detections into %d sources", len(initialDiaSources))
866 self._reorderPeaksBySignificance(initialDiaSources)
868 initialDiaSources = sources
872 for source
in initialDiaSources:
875 initialDiaSources.getTable().setIdFactory(idFactory)
876 initialDiaSources.setMetadata(self.algMetadata)
878 self.metadata[
"nMergedDiaSources"] = len(initialDiaSources)
880 if self.config.doMaskStreaks:
881 streakInfo = self._runStreakMasking(difference)
883 if self.config.doSkySources:
884 self.addSkySources(initialDiaSources, difference.mask, difference.info.id)
886 if self.config.doRejectBadMaskPlaneDetections:
889 initialDiaSources = self._rejectBadMaskedDetections(initialDiaSources, difference.mask)
891 if not initialDiaSources.isContiguous():
892 initialDiaSources = initialDiaSources.copy(deep=
True)
894 self.measureDiaSources(initialDiaSources, science, difference, matchedTemplate)
897 diaSources = self._removeBadSources(initialDiaSources)
899 if self.config.run_sattle:
900 diaSources = self.filterSatellites(diaSources, science)
903 diaSources, trail_parameters = self._find_glint_trails(diaSources)
904 if self.config.writeGlintInfo:
905 measurementResults.mergeItems(trail_parameters,
'glintTrailInfo')
907 if self.config.doForcedMeasurement:
908 self.measureForcedSources(diaSources, science, difference.getWcs())
909 self.measureForcedSources(diaSources, matchedTemplate, difference.getWcs(),
916 difference.image.array[difference.mask.array & difference.mask.getPlaneBitMask(
'NO_DATA') > 0] = 0
918 measurementResults.subtractedMeasuredExposure = difference
920 if self.config.doMaskStreaks
and self.config.writeStreakInfo:
921 measurementResults.maskedStreaks = streakInfo.maskedStreaks
923 if kernelSources
is not None:
924 self.calculateMetrics(science, difference, diaSources, kernelSources)
926 if np.count_nonzero(~diaSources[
"sky_source"]) > 0:
927 measurementResults.diaSources = diaSources
928 elif self.config.raiseOnNoDiaSources:
930 elif len(diaSources) > 0:
933 measurementResults.diaSources = diaSources
934 self.log.info(
"Measured %d diaSources and %d sky sources",
935 np.count_nonzero(~diaSources[
"sky_source"]),
936 np.count_nonzero(diaSources[
"sky_source"])
938 return measurementResults
940 def _reorderPeaksBySignificance(self, diaSources):
941 """Sort each merged source's footprint peaks by significance.
945 diaSources : `lsst.afw.table.SourceCatalog`
946 Merged sources whose footprint peaks are re-ordered in place.
948 for source
in diaSources:
949 peaks = source.getFootprint().getPeaks()
950 if len(peaks) < 2
or "significance" not in peaks.schema.getNames():
952 ordered = sorted(peaks, key=
lambda peak: peak[
"significance"], reverse=
True)
953 newPeaks = afwDetection.PeakCatalog(peaks.table)
954 newPeaks.extend(ordered)
956 source.getFootprint().setPeakCatalog(newPeaks)
958 def _deblend(self, difference, positiveFootprints, negativeFootprints):
959 """Deblend the positive and negative footprints and return a catalog
960 containing just the children, and the deblended footprints.
964 difference : `lsst.afw.image.Exposure`
965 Result of subtracting template from the science image.
966 positiveFootprints, negativeFootprints : `lsst.afw.detection.FootprintSet`
967 Positive and negative polarity footprints measured on
968 ``difference`` to be deblended separately.
972 sources : `lsst.afw.table.SourceCatalog`
973 Positive and negative deblended children.
974 positives, negatives : `lsst.afw.table.SourceCatalog`
975 Deblended positive and negative polarity sources with footprints
976 detected on ``difference``.
979 def deblend(footprints, negative=False):
980 """Deblend a positive or negative footprint set,
981 and return the deblended children.
985 footprints : `lsst.afw.detection.FootprintSet`
987 Set True if the footprints contain negative fluxes
991 sources : `lsst.afw.table.SourceCatalog`
993 sources = afwTable.SourceCatalog(self.schema)
994 footprints.makeSources(sources)
997 difference_inverted = difference.clone()
998 difference_inverted.image *= -1
999 self.deblend.
run(exposure=difference_inverted, sources=sources)
1000 children = sources[sources[
"parent"] != 0]
1002 for child
in children:
1003 footprint = child.getFootprint()
1004 array = footprint.getImageArray()
1007 self.deblend.
run(exposure=difference, sources=sources)
1008 self.setPrimaryFlags.
run(sources)
1009 children = sources[
"detect_isDeblendedSource"] == 1
1010 sources = sources[children].copy(deep=
True)
1012 sources[
'parent'] = 0
1013 return sources.copy(deep=
True)
1015 positives = deblend(positiveFootprints)
1016 negatives = deblend(negativeFootprints, negative=
True)
1018 sources = afwTable.SourceCatalog(self.schema)
1019 sources.reserve(len(positives) + len(negatives))
1020 sources.extend(positives, deep=
True)
1021 sources.extend(negatives, deep=
True)
1022 if len(negatives) > 0:
1023 sources[-len(negatives):][
"is_negative"] =
True
1024 return sources, positives, negatives
1026 def _rejectBadMaskedDetections(self, initialDiaSources, mask):
1027 """Remove detections whose footprint peak lies on a bad-masked pixel.
1029 Detections are rejected if any peak of the source footprint falls on a
1030 pixel with any of the ``config.badMaskPlanes`` bits set.
1034 initialDiaSources : `lsst.afw.table.SourceCatalog`
1035 The catalog of detected peaks.
1036 mask : `lsst.afw.image.Mask`
1037 Mask of the image used for detection.
1041 initialDiaSources : `lsst.afw.table.SourceCatalog`
1042 The catalog with bad-masked detections removed.
1044 if not self.config.badMaskPlanes
or len(initialDiaSources) == 0:
1045 self.metadata[
"nRejectedBadMaskPlanes"] = 0
1046 return initialDiaSources
1048 presentPlanes = [mp
for mp
in self.config.badMaskPlanes
1049 if mp
in mask.getMaskPlaneDict()]
1050 if not presentPlanes:
1051 self.metadata[
"nRejectedBadMaskPlanes"] = 0
1052 return initialDiaSources
1054 badBitMask = mask.getPlaneBitMask(presentPlanes)
1055 x0, y0 = mask.getXY0()
1057 selector = np.ones(len(initialDiaSources), dtype=bool)
1058 for i, source
in enumerate(initialDiaSources):
1059 peaks = source.getFootprint().getPeaks()
1061 ix = peak.getIx() - x0
1062 iy = peak.getIy() - y0
1063 if mask.array[iy, ix] & badBitMask:
1066 nRejected = np.count_nonzero(~selector)
1067 self.metadata[
"nRejectedBadMaskPlanes"] = nRejected
1069 self.log.info(
"Rejected %d detections on pixels with bad mask "
1070 "planes %s before measurement.",
1071 nRejected, presentPlanes)
1072 return initialDiaSources[selector].copy(deep=
True)
1074 def _removeBadSources(self, diaSources):
1075 """Remove unphysical diaSources from the catalog.
1079 diaSources : `lsst.afw.table.SourceCatalog`
1080 The catalog of detected sources.
1084 diaSources : `lsst.afw.table.SourceCatalog`
1085 The updated catalog of detected sources, with any source that has a
1086 flag in ``config.badSourceFlags`` set removed.
1088 selector = np.ones(len(diaSources), dtype=bool)
1089 for flag
in self.config.badSourceFlags:
1090 flags = diaSources[flag]
1091 nBad = np.count_nonzero(flags)
1093 self.log.debug(
"Found %d unphysical sources with flag %s.", nBad, flag)
1095 nBadTotal = np.count_nonzero(~selector)
1096 self.metadata[
"nRemovedBadFlaggedSources"] = nBadTotal
1097 self.log.info(
"Removed %d unphysical sources.", nBadTotal)
1098 return diaSources[selector].copy(deep=
True)
1100 def _find_glint_trails(self, diaSources):
1101 """Define a new flag column for diaSources that are in a glint trail.
1105 diaSources : `lsst.afw.table.SourceCatalog`
1106 The catalog of detected sources.
1110 diaSources : `lsst.afw.table.SourceCatalog`
1111 The updated catalog of detected sources, with a new bool column
1112 called 'glint_trail' added.
1114 trail_parameters : `dict`
1115 Parameters of all the trails that were found.
1117 if self.config.doSkySources:
1119 candidateDiaSources = diaSources[~diaSources[
"sky_source"]].copy(deep=
True)
1121 candidateDiaSources = diaSources
1122 trailed_glints = self.findGlints.
run(candidateDiaSources)
1123 glint_mask = [
True if id
in trailed_glints.trailed_ids
else False for id
in diaSources[
'id']]
1124 if np.any(glint_mask):
1125 diaSources[
'glint_trail'] = np.array(glint_mask)
1127 slopes = np.array([trail.slope
for trail
in trailed_glints.parameters])
1128 intercepts = np.array([trail.intercept
for trail
in trailed_glints.parameters])
1129 stderrs = np.array([trail.stderr
for trail
in trailed_glints.parameters])
1130 lengths = np.array([trail.length
for trail
in trailed_glints.parameters])
1131 angles = np.array([trail.angle
for trail
in trailed_glints.parameters])
1132 parameters = {
'slopes': slopes,
'intercepts': intercepts,
'stderrs': stderrs,
'lengths': lengths,
1135 trail_parameters = pipeBase.Struct(glintTrailInfo=parameters)
1137 return diaSources, trail_parameters
1139 def addSkySources(self, diaSources, mask, seed,
1141 """Add sources in empty regions of the difference image
1142 for measuring the background.
1146 diaSources : `lsst.afw.table.SourceCatalog`
1147 The catalog of detected sources.
1148 mask : `lsst.afw.image.Mask`
1149 Mask plane for determining regions where Sky sources can be added.
1151 Seed value to initialize the random number generator.
1154 subtask = self.skySources
1155 if subtask.config.nSources <= 0:
1156 self.metadata[f
"n_{subtask.getName()}"] = 0
1158 skySourceFootprints = subtask.run(mask=mask, seed=seed, catalog=diaSources)
1159 self.metadata[f
"n_{subtask.getName()}"] = len(skySourceFootprints)
1161 def measureDiaSources(self, diaSources, science, difference, matchedTemplate):
1162 """Use (matched) template and science image to constrain dipole fitting.
1166 diaSources : `lsst.afw.table.SourceCatalog`
1167 The catalog of detected sources.
1168 science : `lsst.afw.image.ExposureF`
1169 Science exposure that the template was subtracted from.
1170 difference : `lsst.afw.image.ExposureF`
1171 Result of subtracting template from the science image.
1172 matchedTemplate : `lsst.afw.image.ExposureF`
1173 Warped and PSF-matched template that was used produce the
1177 for mp
in self.config.measurement.plugins[
"base_PixelFlags"].masksFpAnywhere:
1178 difference.mask.addMaskPlane(mp)
1181 self.measurement.
run(diaSources, difference, science, matchedTemplate)
1182 if self.config.doApCorr:
1183 apCorrMap = difference.getInfo().getApCorrMap()
1184 if apCorrMap
is None:
1185 self.log.warning(
"Difference image does not have valid aperture correction; skipping.")
1187 self.applyApCorr.
run(
1189 apCorrMap=apCorrMap,
1192 def measureForcedSources(self, diaSources, image, wcs, template=False):
1193 """Perform forced measurement of the diaSources on a direct image.
1197 diaSources : `lsst.afw.table.SourceCatalog`
1198 The catalog of detected sources.
1199 image: `lsst.afw.image.ExposureF`
1200 Exposure that the forced measurement is being performed on
1201 wcs : `lsst.afw.geom.SkyWcs`
1202 Coordinate system definition (wcs) for the exposure.
1204 Is the forced measurement being performed on the template?
1208 forcedSources = self.forcedMeasurement.generateMeasCat(image, diaSources, wcs)
1209 self.forcedMeasurement.
run(forcedSources, image, diaSources, wcs)
1212 base_key =
'ip_diffim_forced_template_PsfFlux'
1214 base_key =
'ip_diffim_forced_PsfFlux'
1215 mapper = afwTable.SchemaMapper(forcedSources.schema, diaSources.schema)
1216 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFlux")[0],
1217 f
"{base_key}_instFlux",
True)
1218 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFluxErr")[0],
1219 f
"{base_key}_instFluxErr",
True)
1220 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_area")[0],
1221 f
"{base_key}_area",
True)
1222 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag")[0],
1223 f
"{base_key}_flag",
True)
1224 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_noGoodPixels")[0],
1225 f
"{base_key}_flag_noGoodPixels",
True)
1226 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_edge")[0],
1227 f
"{base_key}_flag_edge",
True)
1228 for diaSource, forcedSource
in zip(diaSources, forcedSources):
1229 diaSource.assign(forcedSource, mapper)
1231 def findAndMaskCosmicRays(self, difference):
1232 """Detect and mask cosmic rays on the difference image.
1236 difference : `lsst.afw.image.Exposure`
1237 The background-subtracted difference image.
1238 The mask plane will be modified in place.
1244 crs = findCosmicRays(
1245 difference.maskedImage,
1248 pexConfig.makePropertySet(self.config.cosmicray),
1249 self.config.cosmicray.keepCRs,
1255 mask = difference.maskedImage.mask
1256 crBit = mask.getPlaneBitMask(
"CR")
1257 afwDetection.setMaskFromFootprintList(mask, crs, crBit)
1260 text =
"masked" if self.config.cosmicray.keepCRs
else "interpolated over"
1261 self.log.info(
"Identified and %s %s cosmic rays.", text, num)
1262 self.metadata[
"cosmic_ray_count"] = num
1264 def calculateMetrics(self, science, difference, diaSources, kernelSources):
1265 """Add difference image QA metrics to the Task metadata.
1267 This may be used to produce corresponding metrics (see
1268 lsst.analysis.tools.tasks.diffimTaskDetectorVisitMetricAnalysis).
1272 science : `lsst.afw.image.ExposureF`
1273 Science exposure that was subtracted.
1274 difference : `lsst.afw.image.Exposure`
1275 The target difference image to calculate metrics for.
1276 diaSources : `lsst.afw.table.SourceCatalog`
1277 The catalog of detected sources.
1278 kernelSources : `lsst.afw.table.SourceCatalog`
1279 Final selection of sources that was used for psf matching.
1281 mask = difference.mask
1282 badPix = (mask.array & mask.getPlaneBitMask(self.config.detection.excludeMaskPlanes)) > 0
1283 self.metadata[
"nGoodPixels"] = np.sum(~badPix)
1284 self.metadata[
"nBadPixels"] = np.sum(badPix)
1285 detPosPix = (mask.array & mask.getPlaneBitMask(
"DETECTED")) > 0
1286 detNegPix = (mask.array & mask.getPlaneBitMask(
"DETECTED_NEGATIVE")) > 0
1287 self.metadata[
"nPixelsDetectedPositive"] = np.sum(detPosPix)
1288 self.metadata[
"nPixelsDetectedNegative"] = np.sum(detNegPix)
1291 self.metadata[
"nBadPixelsDetectedPositive"] = np.sum(detPosPix)
1292 self.metadata[
"nBadPixelsDetectedNegative"] = np.sum(detNegPix)
1294 metricsMaskPlanes = list(mask.getMaskPlaneDict().keys())
1295 for maskPlane
in metricsMaskPlanes:
1297 self.metadata[
"%s_mask_fraction"%maskPlane.lower()] = evaluateMaskFraction(mask, maskPlane)
1298 except InvalidParameterError:
1299 self.metadata[
"%s_mask_fraction"%maskPlane.lower()] = -1
1300 self.log.info(
"Unable to calculate metrics for mask plane %s: not in image"%maskPlane)
1302 if self.config.doSkySources:
1303 skySources = diaSources[diaSources[
"sky_source"]]
1306 metrics = computeDifferenceImageMetrics(science, difference, kernelSources, sky_sources=skySources)
1308 self.metadata[
"residualFootprintRatioMean"] = metrics.differenceFootprintRatioMean
1309 self.metadata[
"residualFootprintRatioStdev"] = metrics.differenceFootprintRatioStdev
1310 self.metadata[
"differenceFootprintSkyRatioMean"] = metrics.differenceFootprintSkyRatioMean
1311 self.metadata[
"differenceFootprintSkyRatioStdev"] = metrics.differenceFootprintSkyRatioStdev
1312 self.log.info(
"Mean, stdev of ratio of difference to science "
1313 "pixels in star footprints: %5.4f, %5.4f",
1314 self.metadata[
"residualFootprintRatioMean"],
1315 self.metadata[
"residualFootprintRatioStdev"])
1316 if self.config.raiseOnBadSubtractionRatio:
1317 if metrics.differenceFootprintRatioMean > self.config.badSubtractionRatioThreshold:
1319 threshold=self.config.badSubtractionRatioThreshold)
1320 if metrics.differenceFootprintRatioStdev > self.config.badSubtractionVariationThreshold:
1322 threshold=self.config.badSubtractionVariationThreshold)
1324 def getSattleDiaSourceAllowlist(self, diaSources, science):
1325 """Query the sattle service and determine which diaSources are allowed.
1329 diaSources : `lsst.afw.table.SourceCatalog`
1330 The catalog of detected sources.
1331 science : `lsst.afw.image.ExposureF`
1332 Science exposure that was subtracted.
1336 allow_list : `list` of `int`
1337 diaSourceIds of diaSources that can be made public.
1342 Raised if sattle call does not return success.
1344 wcs = science.getWcs()
1345 visit_info = science.getInfo().getVisitInfo()
1346 visit_id = visit_info.getId()
1347 sattle_uri_base = os.getenv(
'SATTLE_URI_BASE')
1349 dia_sources_json = []
1350 for source
in diaSources:
1351 source_bbox = source.getFootprint().getBBox()
1352 corners = wcs.pixelToSky([
lsst.geom.Point2D(c)
for c
in source_bbox.getCorners()])
1353 bbox_radec = [[pt.getRa().asDegrees(), pt.getDec().asDegrees()]
for pt
in corners]
1354 dia_sources_json.append({
"diasource_id": source[
"id"],
"bbox": bbox_radec})
1356 payload = {
"visit_id": visit_id,
"detector_id": science.getDetector().getId(),
1357 "diasources": dia_sources_json,
"historical": self.config.sattle_historical}
1359 sattle_output = requests.put(f
'{sattle_uri_base}/diasource_allow_list',
1363 if sattle_output.status_code == 404:
1364 self.log.warning(f
'Visit {visit_id} not found in sattle cache, re-sending')
1365 populate_sattle_visit_cache(visit_info, historical=self.config.sattle_historical)
1366 sattle_output = requests.put(f
'{sattle_uri_base}/diasource_allow_list', json=payload)
1368 sattle_output.raise_for_status()
1370 return sattle_output.json()[
'allow_list']
1372 def filterSatellites(self, diaSources, science):
1373 """Remove diaSources overlapping predicted satellite positions.
1377 diaSources : `lsst.afw.table.SourceCatalog`
1378 The catalog of detected sources.
1379 science : `lsst.afw.image.ExposureF`
1380 Science exposure that was subtracted.
1384 filterdDiaSources : `lsst.afw.table.SourceCatalog`
1385 Filtered catalog of diaSources
1388 allow_list = self.getSattleDiaSourceAllowlist(diaSources, science)
1391 allow_set = set(allow_list)
1392 allowed_ids = [source[
'id']
in allow_set
for source
in diaSources]
1393 diaSources = diaSources[np.array(allowed_ids)].copy(deep=
True)
1395 self.log.warning(
'Sattle allowlist is empty, all diaSources removed')
1396 diaSources = diaSources[0:0].copy(deep=
True)
1399 def _runStreakMasking(self, difference):
1400 """Do streak masking and optionally save the resulting streak
1401 fit parameters in a catalog.
1403 Only returns non-empty streakInfo if self.config.writeStreakInfo
1404 is set. The difference image is binned by self.config.streakBinFactor
1405 (and detection is run a second time) so that regions with lower
1406 surface brightness streaks are more likely to fall above the
1407 detection threshold.
1411 difference: `lsst.afw.image.Exposure`
1412 The exposure in which to search for streaks. Must have a detection
1417 streakInfo: `lsst.pipe.base.Struct`
1418 ``rho`` : `np.ndarray`
1419 Angle of detected streak.
1420 ``theta`` : `np.ndarray`
1421 Distance from center of detected streak.
1422 ``sigma`` : `np.ndarray`
1423 Width of streak profile.
1424 ``reducedChi2`` : `np.ndarray`
1425 Reduced chi2 of the best-fit streak profile.
1426 ``modelMaximum`` : `np.ndarray`
1427 Peak value of the fit line profile.
1429 maskedImage = difference.maskedImage
1432 self.config.streakBinFactor,
1433 self.config.streakBinFactor)
1434 binnedExposure = afwImage.ExposureF(binnedMaskedImage.getBBox())
1435 binnedExposure.setMaskedImage(binnedMaskedImage)
1437 binnedExposure.mask &= ~binnedExposure.mask.getPlaneBitMask(
'DETECTED')
1439 sigma = difference.psf.computeShape(difference.psf.getAveragePosition()).getDeterminantRadius()
1440 _table = afwTable.SourceTable.make(afwTable.SourceTable.makeMinimalSchema())
1441 self.streakDetection.
run(table=_table, exposure=binnedExposure, doSmooth=
True,
1442 sigma=sigma/self.config.streakBinFactor)
1443 binnedDetectedMaskPlane = binnedExposure.mask.array & binnedExposure.mask.getPlaneBitMask(
'DETECTED')
1444 rescaledDetectedMaskPlane = binnedDetectedMaskPlane.repeat(self.config.streakBinFactor,
1445 axis=0).repeat(self.config.streakBinFactor,
1448 streakMaskedImage = maskedImage.clone()
1449 ysize, xsize = rescaledDetectedMaskPlane.shape
1450 streakMaskedImage.mask.array[:ysize, :xsize] |= rescaledDetectedMaskPlane
1452 streaks = self.maskStreaks.
run(streakMaskedImage)
1453 streakMaskPlane = streakMaskedImage.mask.array & streakMaskedImage.mask.getPlaneBitMask(
'STREAK')
1455 maskedImage.mask.array |= streakMaskPlane
1457 if self.config.writeStreakInfo:
1458 rhos = np.array([line.rho
for line
in streaks.lines])
1459 thetas = np.array([line.theta
for line
in streaks.lines])
1460 sigmas = np.array([line.sigma
for line
in streaks.lines])
1461 chi2s = np.array([line.reducedChi2
for line
in streaks.lines])
1462 modelMaximums = np.array([line.modelMaximum
for line
in streaks.lines])
1463 streakInfo = {
'rho': rhos,
'theta': thetas,
'sigma': sigmas,
'reducedChi2': chi2s,
1464 'modelMaximum': modelMaximums}
1466 streakInfo = {
'rho': np.array([]),
'theta': np.array([]),
'sigma': np.array([]),
1467 'reducedChi2': np.array([]),
'modelMaximum': np.array([])}
1468 return pipeBase.Struct(maskedStreaks=streakInfo)
1472 scoreExposure = pipeBase.connectionTypes.Input(
1473 doc=
"Maximum likelihood image for detection.",
1474 dimensions=(
"instrument",
"visit",
"detector"),
1475 storageClass=
"ExposureF",
1476 name=
"{fakesType}{coaddName}Diff_scoreTempExp",
1478 scoreMeasuredExposure = pipeBase.connectionTypes.Output(
1479 doc=
"Score image after backgrond subtraction and detection.",
1480 dimensions=(
"instrument",
"visit",
"detector"),
1481 storageClass=
"ExposureF",
1482 name=
"{fakesType}{coaddName}Diff_scoreExp",
1486class DetectAndMeasureScoreConfig(DetectAndMeasureConfig,
1487 pipelineConnections=DetectAndMeasureScoreConnections):
1491class DetectAndMeasureScoreTask(DetectAndMeasureTask):
1492 """Detect DIA sources using a score image,
1493 and measure the detections on the difference image.
1495 Source detection is run on the supplied score, or maximum likelihood,
1496 image. Note that no additional convolution will be done in this case.
1497 Close positive and negative detections will optionally be merged into
1499 Sky sources, or forced detections in background regions, will optionally
1500 be added, and the configured measurement algorithm will be run on all
1503 ConfigClass = DetectAndMeasureScoreConfig
1504 _DefaultName =
"detectAndMeasureScore"
1507 def run(self, science, matchedTemplate, difference, scoreExposure, kernelSources,
1508 idFactory=None, measurementResults=None):
1509 """Detect and measure sources on a score image.
1513 science : `lsst.afw.image.ExposureF`
1514 Science exposure that the template was subtracted from.
1515 matchedTemplate : `lsst.afw.image.ExposureF`
1516 Warped and PSF-matched template that was used produce the
1518 difference : `lsst.afw.image.ExposureF`
1519 Result of subtracting template from the science image.
1520 scoreExposure : `lsst.afw.image.ExposureF`
1521 Score or maximum likelihood difference image
1522 kernelSources : `lsst.afw.table.SourceCatalog`
1523 Final selection of sources that was used for psf matching.
1524 idFactory : `lsst.afw.table.IdFactory`, optional
1525 Generator object used to assign ids to detected sources in the
1526 difference image. Ids from this generator are not set until after
1527 deblending and merging positive/negative peaks.
1528 measurementResults : `lsst.pipe.base.Struct`, optional
1529 Result struct that is modified to allow saving of partial outputs
1530 for some failure conditions. If the task completes successfully,
1531 this is also returned.
1535 measurementResults : `lsst.pipe.base.Struct`
1537 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
1538 Subtracted exposure with detection mask applied.
1539 ``scoreMeasuredExposure`` : `lsst.afw.image.ExposureF`
1540 Score exposure with detection mask applied.
1541 ``diaSources`` : `lsst.afw.table.SourceCatalog`
1542 The catalog of detected sources.
1543 ``differenceBackground`` : `lsst.afw.math.BackgroundList`
1544 Background that was subtracted from the difference image.
1546 if measurementResults
is None:
1547 measurementResults = pipeBase.Struct()
1548 if idFactory
is None:
1549 idFactory = lsst.meas.base.IdGenerator().make_table_id_factory()
1552 self._prepareInputs(difference)
1553 self._prepareInputs(scoreExposure)
1554 if self.config.doSubtractBackground:
1555 background = self._fitAndSubtractBackground(
1556 differenceExposure=difference, scoreExposure=scoreExposure,
1559 background = afwMath.BackgroundList()
1561 if self.config.doFindCosmicRays:
1564 self.findAndMaskCosmicRays(difference)
1565 scoreExposure.mask.array |= difference.mask.array
1570 table = afwTable.SourceTable.make(self.schema)
1571 detectResults = self.detection.
run(
1573 exposure=scoreExposure,
1575 background=background,
1578 measurementResults.differenceBackground = background
1579 measurementResults.scoreMeasuredExposure = scoreExposure
1584 detectedBit = difference.mask.getPlaneBitMask([
"DETECTED"])
1585 detectedPix = scoreExposure.mask.array & detectedBit > 0
1586 detectedNegativeBit = difference.mask.getPlaneBitMask([
"DETECTED_NEGATIVE"])
1587 detectedNegativePix = scoreExposure.mask.array & detectedNegativeBit > 0
1588 difference.mask.array[detectedPix] |= detectedBit
1589 difference.mask.array[detectedNegativePix] |= detectedNegativeBit
1591 edgeBit = difference.mask.getPlaneBitMask([
"EDGE"])
1592 edgePix = scoreExposure.mask.array & edgeBit > 0
1593 difference.mask.array[edgePix] |= edgeBit
1595 if self.config.doDeblend:
1596 sources, positives, negatives = self._deblend(difference,
1597 detectResults.positive,
1598 detectResults.negative)
1600 self.processResults(science, matchedTemplate, difference,
1601 sources, idFactory, kernelSources,
1602 positives=positives,
1603 negatives=negatives,
1604 measurementResults=measurementResults)
1607 positives = afwTable.SourceCatalog(self.schema)
1608 detectResults.positive.makeSources(positives)
1609 negatives = afwTable.SourceCatalog(self.schema)
1610 detectResults.negative.makeSources(negatives)
1611 self.processResults(science, matchedTemplate, difference,
1612 detectResults.sources, idFactory, kernelSources,
1613 positives=positives,
1614 negatives=negatives,
1615 measurementResults=measurementResults)
1616 return measurementResults
__init__(self, *, ratio, threshold)
__init__(self, maxCosmicRays, **kwargs)
std::shared_ptr< ImageT > binImage(ImageT const &inImage, int const binX, int const binY, lsst::afw::math::Property const flags=lsst::afw::math::MEAN)
run(self, *, coaddExposureHandles, bbox, wcs, dataIds, physical_filter, visit=None)