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_saturated_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",
"SATURATED_TEMPLATE",
"SPIKE"]
481 self.measurement.plugins[
"base_PixelFlags"].masksFpCenter = [
482 "STREAK",
"INJECTED",
"INJECTED_TEMPLATE",
"HIGH_VARIANCE",
"SATURATED_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")
580 for flag
in self.config.badSourceFlags:
581 if flag
not in self.schema:
582 raise pipeBase.InvalidQuantumError(
"Field %s not in schema" % flag)
585 self.outputSchema = afwTable.SourceCatalog(self.schema)
586 self.outputSchema.getTable().setMetadata(self.algMetadata)
588 def runQuantum(self, butlerQC: pipeBase.QuantumContext,
589 inputRefs: pipeBase.InputQuantizedConnection,
590 outputRefs: pipeBase.OutputQuantizedConnection):
591 inputs = butlerQC.get(inputRefs)
592 idGenerator = self.config.idGenerator.apply(butlerQC.quantum.dataId)
593 idFactory = idGenerator.make_table_id_factory()
596 measurementResults = pipeBase.Struct(
597 subtractedMeasuredExposure=
None,
600 differenceBackground=
None,
603 self.run(**inputs, idFactory=idFactory, measurementResults=measurementResults)
604 except pipeBase.AlgorithmError
as e:
605 error = pipeBase.AnnotatedPartialOutputsError.annotate(
608 measurementResults.subtractedMeasuredExposure,
609 measurementResults.diaSources,
610 measurementResults.maskedStreaks,
613 butlerQC.put(measurementResults, outputRefs)
615 butlerQC.put(measurementResults, outputRefs)
618 def run(self, science, matchedTemplate, difference, kernelSources=None,
619 idFactory=None, measurementResults=None):
620 """Detect and measure sources on a difference image.
622 The difference image will be convolved with a gaussian approximation of
623 the PSF to form a maximum likelihood image for detection.
624 Close positive and negative detections will optionally be merged into
626 Sky sources, or forced detections in background regions, will optionally
627 be added, and the configured measurement algorithm will be run on all
632 science : `lsst.afw.image.ExposureF`
633 Science exposure that the template was subtracted from.
634 matchedTemplate : `lsst.afw.image.ExposureF`
635 Warped and PSF-matched template that was used produce the
637 difference : `lsst.afw.image.ExposureF`
638 Result of subtracting template from the science image.
639 kernelSources : `lsst.afw.table.SourceCatalog`, optional
640 Final selection of sources that was used for psf matching.
641 idFactory : `lsst.afw.table.IdFactory`, optional
642 Generator object used to assign ids to detected sources in the
643 difference image. Ids from this generator are not set until after
644 deblending and merging positive/negative peaks.
645 measurementResults : `lsst.pipe.base.Struct`, optional
646 Result struct that is modified to allow saving of partial outputs
647 for some failure conditions. If the task completes successfully,
648 this is also returned.
652 measurementResults : `lsst.pipe.base.Struct`
654 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
655 Subtracted exposure with detection mask applied.
656 ``diaSources`` : `lsst.afw.table.SourceCatalog`
657 The catalog of detected sources.
658 ``differenceBackground`` : `lsst.afw.math.BackgroundList`
659 Background that was subtracted from the difference image.
661 if measurementResults
is None:
662 measurementResults = pipeBase.Struct()
663 if idFactory
is None:
664 idFactory = lsst.meas.base.IdGenerator().make_table_id_factory()
667 self._prepareInputs(difference)
668 if self.config.doSubtractBackground:
669 background = self._fitAndSubtractBackground(differenceExposure=difference)
671 background = afwMath.BackgroundList()
673 if self.config.doFindCosmicRays:
674 self.findAndMaskCosmicRays(difference)
679 table = afwTable.SourceTable.make(self.schema)
680 results = self.detection.
run(
684 background=background,
687 measurementResults.differenceBackground = background
689 if self.config.doDeblend:
690 sources, positives, negatives = self._deblend(difference,
695 positives = afwTable.SourceCatalog(self.schema)
696 results.positive.makeSources(positives)
697 negatives = afwTable.SourceCatalog(self.schema)
698 results.negative.makeSources(negatives)
699 sources = results.sources
701 self.processResults(science, matchedTemplate, difference,
702 sources, idFactory, kernelSources,
705 measurementResults=measurementResults)
706 return measurementResults
708 def _prepareInputs(self, difference):
709 """Ensure that we start with an empty detection and deblended mask.
713 difference : `lsst.afw.image.ExposureF`
714 The difference image that will be used for detecting diaSources.
715 The mask plane will be modified in place.
719 lsst.pipe.base.UpstreamFailureNoWorkFound
720 If the PSF is not usable for measurement.
723 sigma = difference.psf.computeShape(difference.psf.getAveragePosition()).getDeterminantRadius()
725 raise pipeBase.UpstreamFailureNoWorkFound(
"Invalid PSF detected! PSF width evaluates to NaN.")
727 mask = difference.mask
728 for mp
in self.config.clearMaskPlanes:
729 if mp
not in mask.getMaskPlaneDict():
730 mask.addMaskPlane(mp)
731 mask &= ~mask.getPlaneBitMask(self.config.clearMaskPlanes)
733 def _fitAndSubtractBackground(self, differenceExposure, scoreExposure=None):
734 """Fit the final background to ``differenceExposure``.
736 A rough background is first fit on the difference and an internal
737 first-pass detection is run on the (background-subtracted) clone; the
738 resulting detection mask is then transferred to ``differenceExposure``
739 so the final background fit can mask out real sources. This two-pass
740 scheme is what suppresses spurious detections from glints and
741 extended structures: the small-binsize initial fit aggressively
742 over-subtracts those features, and the resulting peak mask lets the
743 large-binsize final fit produce a clean background. When
744 ``scoreExposure`` is supplied (score-image variant), the final
745 background is also subtracted from its image so the caller's final
746 detection runs on a properly background-subtracted score.
748 The first-pass detection's `Struct` is discarded; only the mask it
749 leaves on the clone is used downstream.
753 differenceExposure : `lsst.afw.image.ExposureF`
754 Difference image. The final background is subtracted from its
755 image in place but its mask is not modified.
756 scoreExposure : `lsst.afw.image.ExposureF`, optional
757 Maximum-likelihood score image. When supplied, the final
758 background is also subtracted from its image.
762 background : `lsst.afw.math.BackgroundList`
763 The fit final background.
765 if scoreExposure
is None:
766 detectionExposure = differenceExposure.clone()
767 background = self.subtractInitialBackground.
run(detectionExposure).background
772 background = self.subtractInitialBackground.
run(differenceExposure.clone()).background
773 detectionExposure = scoreExposure.clone()
774 detectionExposure.image -= background.getImage()
776 table = afwTable.SourceTable.make(self.schema)
779 exposure=detectionExposure,
781 background=background,
787 detectedBit = differenceExposure.mask.getPlaneBitMask([
"DETECTED"])
788 detectedPix = detectionExposure.mask.array & detectedBit > 0
789 detectedNegativeBit = differenceExposure.mask.getPlaneBitMask([
"DETECTED_NEGATIVE"])
790 detectedNegativePix = detectionExposure.mask.array & detectedNegativeBit > 0
791 differenceExposure.mask.array[detectedPix] |= detectedBit
792 differenceExposure.mask.array[detectedNegativePix] |= detectedNegativeBit
793 background = self.subtractFinalBackground.
run(differenceExposure).background
794 if scoreExposure
is not None:
797 scoreExposure.image -= background.getImage()
800 def processResults(self, science, matchedTemplate, difference, sources, idFactory,
801 kernelSources=None, positives=None, negatives=None, measurementResults=None):
802 """Measure and process the results of source detection.
806 science : `lsst.afw.image.ExposureF`
807 Science exposure that the template was subtracted from.
808 matchedTemplate : `lsst.afw.image.ExposureF`
809 Warped and PSF-matched template that was used produce the
811 difference : `lsst.afw.image.ExposureF`
812 Result of subtracting template from the science image.
813 sources : `lsst.afw.table.SourceCatalog`
814 Detected sources on the difference exposure.
815 idFactory : `lsst.afw.table.IdFactory`
816 Generator object used to assign ids to detected sources in the
818 kernelSources : `lsst.afw.table.SourceCatalog`, optional
819 Final selection of sources that was used for psf matching.
820 positives : `lsst.afw.table.SourceCatalog`, optional
821 Positive polarity footprints.
822 negatives : `lsst.afw.table.SourceCatalog`, optional
823 Negative polarity footprints.
824 measurementResults : `lsst.pipe.base.Struct`, optional
825 Result struct that is modified to allow saving of partial outputs
826 for some failure conditions. If the task completes successfully,
827 this is also returned.
831 measurementResults : `lsst.pipe.base.Struct`
833 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
834 Subtracted exposure with detection mask applied.
835 ``diaSources`` : `lsst.afw.table.SourceCatalog`
836 The catalog of detected sources.
838 if measurementResults
is None:
839 measurementResults = pipeBase.Struct()
840 self.metadata[
"nUnmergedDiaSources"] = len(sources)
841 if self.config.doMerge:
843 if len(positives) > 0:
844 peakSchema = positives[0].getFootprint().peaks.schema
845 elif len(negatives) > 0:
846 peakSchema = negatives[0].getFootprint().peaks.schema
848 peakSchema = afwDetection.PeakTable.makeMinimalSchema()
849 mergeList = afwDetection.FootprintMergeList(self.schema,
850 [
"positive",
"negative"], peakSchema)
851 initialDiaSources = afwTable.SourceCatalog(self.schema)
855 mergeList.addCatalog(initialDiaSources.table, positives,
"positive", minNewPeakDist=0)
856 mergeList.addCatalog(initialDiaSources.table, negatives,
"negative", minNewPeakDist=0)
857 mergeList.getFinalSources(initialDiaSources)
860 initialDiaSources[
"is_negative"] = initialDiaSources[
"merge_footprint_negative"] & \
861 ~initialDiaSources[
"merge_footprint_positive"]
862 self.log.info(
"Merging detections into %d sources", len(initialDiaSources))
864 initialDiaSources = sources
868 for source
in initialDiaSources:
871 initialDiaSources.getTable().setIdFactory(idFactory)
872 initialDiaSources.setMetadata(self.algMetadata)
874 self.metadata[
"nMergedDiaSources"] = len(initialDiaSources)
876 if self.config.doMaskStreaks:
877 streakInfo = self._runStreakMasking(difference)
879 if self.config.doSkySources:
880 self.addSkySources(initialDiaSources, difference.mask, difference.info.id)
882 if self.config.doRejectBadMaskPlaneDetections:
885 initialDiaSources = self._rejectBadMaskedDetections(initialDiaSources, difference.mask)
887 if not initialDiaSources.isContiguous():
888 initialDiaSources = initialDiaSources.copy(deep=
True)
890 self.measureDiaSources(initialDiaSources, science, difference, matchedTemplate)
893 diaSources = self._removeBadSources(initialDiaSources)
895 if self.config.run_sattle:
896 diaSources = self.filterSatellites(diaSources, science)
899 diaSources, trail_parameters = self._find_glint_trails(diaSources)
900 if self.config.writeGlintInfo:
901 measurementResults.mergeItems(trail_parameters,
'glintTrailInfo')
903 if self.config.doForcedMeasurement:
904 self.measureForcedSources(diaSources, science, difference.getWcs())
905 self.measureForcedSources(diaSources, matchedTemplate, difference.getWcs(),
912 difference.image.array[difference.mask.array & difference.mask.getPlaneBitMask(
'NO_DATA') > 0] = 0
914 measurementResults.subtractedMeasuredExposure = difference
916 if self.config.doMaskStreaks
and self.config.writeStreakInfo:
917 measurementResults.maskedStreaks = streakInfo.maskedStreaks
919 if kernelSources
is not None:
920 self.calculateMetrics(science, difference, diaSources, kernelSources)
922 if np.count_nonzero(~diaSources[
"sky_source"]) > 0:
923 measurementResults.diaSources = diaSources
924 elif self.config.raiseOnNoDiaSources:
926 elif len(diaSources) > 0:
929 measurementResults.diaSources = diaSources
930 self.log.info(
"Measured %d diaSources and %d sky sources",
931 np.count_nonzero(~diaSources[
"sky_source"]),
932 np.count_nonzero(diaSources[
"sky_source"])
934 return measurementResults
936 def _deblend(self, difference, positiveFootprints, negativeFootprints):
937 """Deblend the positive and negative footprints and return a catalog
938 containing just the children, and the deblended footprints.
942 difference : `lsst.afw.image.Exposure`
943 Result of subtracting template from the science image.
944 positiveFootprints, negativeFootprints : `lsst.afw.detection.FootprintSet`
945 Positive and negative polarity footprints measured on
946 ``difference`` to be deblended separately.
950 sources : `lsst.afw.table.SourceCatalog`
951 Positive and negative deblended children.
952 positives, negatives : `lsst.afw.table.SourceCatalog`
953 Deblended positive and negative polarity sources with footprints
954 detected on ``difference``.
957 def deblend(footprints, negative=False):
958 """Deblend a positive or negative footprint set,
959 and return the deblended children.
963 footprints : `lsst.afw.detection.FootprintSet`
965 Set True if the footprints contain negative fluxes
969 sources : `lsst.afw.table.SourceCatalog`
971 sources = afwTable.SourceCatalog(self.schema)
972 footprints.makeSources(sources)
975 difference_inverted = difference.clone()
976 difference_inverted.image *= -1
977 self.deblend.
run(exposure=difference_inverted, sources=sources)
978 children = sources[sources[
"parent"] != 0]
980 for child
in children:
981 footprint = child.getFootprint()
982 array = footprint.getImageArray()
985 self.deblend.
run(exposure=difference, sources=sources)
986 self.setPrimaryFlags.
run(sources)
987 children = sources[
"detect_isDeblendedSource"] == 1
988 sources = sources[children].copy(deep=
True)
990 sources[
'parent'] = 0
991 return sources.copy(deep=
True)
993 positives = deblend(positiveFootprints)
994 negatives = deblend(negativeFootprints, negative=
True)
996 sources = afwTable.SourceCatalog(self.schema)
997 sources.reserve(len(positives) + len(negatives))
998 sources.extend(positives, deep=
True)
999 sources.extend(negatives, deep=
True)
1000 if len(negatives) > 0:
1001 sources[-len(negatives):][
"is_negative"] =
True
1002 return sources, positives, negatives
1004 def _rejectBadMaskedDetections(self, initialDiaSources, mask):
1005 """Remove detections whose footprint peak lies on a bad-masked pixel.
1007 Detections are rejected if any peak of the source footprint falls on a
1008 pixel with any of the ``config.badMaskPlanes`` bits set.
1012 initialDiaSources : `lsst.afw.table.SourceCatalog`
1013 The catalog of detected peaks.
1014 mask : `lsst.afw.image.Mask`
1015 Mask of the image used for detection.
1019 initialDiaSources : `lsst.afw.table.SourceCatalog`
1020 The catalog with bad-masked detections removed.
1022 if not self.config.badMaskPlanes
or len(initialDiaSources) == 0:
1023 self.metadata[
"nRejectedBadMaskPlanes"] = 0
1024 return initialDiaSources
1026 presentPlanes = [mp
for mp
in self.config.badMaskPlanes
1027 if mp
in mask.getMaskPlaneDict()]
1028 if not presentPlanes:
1029 self.metadata[
"nRejectedBadMaskPlanes"] = 0
1030 return initialDiaSources
1032 badBitMask = mask.getPlaneBitMask(presentPlanes)
1033 x0, y0 = mask.getXY0()
1035 selector = np.ones(len(initialDiaSources), dtype=bool)
1036 for i, source
in enumerate(initialDiaSources):
1037 peaks = source.getFootprint().getPeaks()
1039 ix = peak.getIx() - x0
1040 iy = peak.getIy() - y0
1041 if mask.array[iy, ix] & badBitMask:
1044 nRejected = np.count_nonzero(~selector)
1045 self.metadata[
"nRejectedBadMaskPlanes"] = nRejected
1047 self.log.info(
"Rejected %d detections on pixels with bad mask "
1048 "planes %s before measurement.",
1049 nRejected, presentPlanes)
1050 return initialDiaSources[selector].copy(deep=
True)
1052 def _removeBadSources(self, diaSources):
1053 """Remove unphysical diaSources from the catalog.
1057 diaSources : `lsst.afw.table.SourceCatalog`
1058 The catalog of detected sources.
1062 diaSources : `lsst.afw.table.SourceCatalog`
1063 The updated catalog of detected sources, with any source that has a
1064 flag in ``config.badSourceFlags`` set removed.
1066 selector = np.ones(len(diaSources), dtype=bool)
1067 for flag
in self.config.badSourceFlags:
1068 flags = diaSources[flag]
1069 nBad = np.count_nonzero(flags)
1071 self.log.debug(
"Found %d unphysical sources with flag %s.", nBad, flag)
1073 nBadTotal = np.count_nonzero(~selector)
1074 self.metadata[
"nRemovedBadFlaggedSources"] = nBadTotal
1075 self.log.info(
"Removed %d unphysical sources.", nBadTotal)
1076 return diaSources[selector].copy(deep=
True)
1078 def _find_glint_trails(self, diaSources):
1079 """Define a new flag column for diaSources that are in a glint trail.
1083 diaSources : `lsst.afw.table.SourceCatalog`
1084 The catalog of detected sources.
1088 diaSources : `lsst.afw.table.SourceCatalog`
1089 The updated catalog of detected sources, with a new bool column
1090 called 'glint_trail' added.
1092 trail_parameters : `dict`
1093 Parameters of all the trails that were found.
1095 if self.config.doSkySources:
1097 candidateDiaSources = diaSources[~diaSources[
"sky_source"]].copy(deep=
True)
1099 candidateDiaSources = diaSources
1100 trailed_glints = self.findGlints.
run(candidateDiaSources)
1101 glint_mask = [
True if id
in trailed_glints.trailed_ids
else False for id
in diaSources[
'id']]
1102 if np.any(glint_mask):
1103 diaSources[
'glint_trail'] = np.array(glint_mask)
1105 slopes = np.array([trail.slope
for trail
in trailed_glints.parameters])
1106 intercepts = np.array([trail.intercept
for trail
in trailed_glints.parameters])
1107 stderrs = np.array([trail.stderr
for trail
in trailed_glints.parameters])
1108 lengths = np.array([trail.length
for trail
in trailed_glints.parameters])
1109 angles = np.array([trail.angle
for trail
in trailed_glints.parameters])
1110 parameters = {
'slopes': slopes,
'intercepts': intercepts,
'stderrs': stderrs,
'lengths': lengths,
1113 trail_parameters = pipeBase.Struct(glintTrailInfo=parameters)
1115 return diaSources, trail_parameters
1117 def addSkySources(self, diaSources, mask, seed,
1119 """Add sources in empty regions of the difference image
1120 for measuring the background.
1124 diaSources : `lsst.afw.table.SourceCatalog`
1125 The catalog of detected sources.
1126 mask : `lsst.afw.image.Mask`
1127 Mask plane for determining regions where Sky sources can be added.
1129 Seed value to initialize the random number generator.
1132 subtask = self.skySources
1133 if subtask.config.nSources <= 0:
1134 self.metadata[f
"n_{subtask.getName()}"] = 0
1136 skySourceFootprints = subtask.run(mask=mask, seed=seed, catalog=diaSources)
1137 self.metadata[f
"n_{subtask.getName()}"] = len(skySourceFootprints)
1139 def measureDiaSources(self, diaSources, science, difference, matchedTemplate):
1140 """Use (matched) template and science image to constrain dipole fitting.
1144 diaSources : `lsst.afw.table.SourceCatalog`
1145 The catalog of detected sources.
1146 science : `lsst.afw.image.ExposureF`
1147 Science exposure that the template was subtracted from.
1148 difference : `lsst.afw.image.ExposureF`
1149 Result of subtracting template from the science image.
1150 matchedTemplate : `lsst.afw.image.ExposureF`
1151 Warped and PSF-matched template that was used produce the
1155 for mp
in self.config.measurement.plugins[
"base_PixelFlags"].masksFpAnywhere:
1156 difference.mask.addMaskPlane(mp)
1159 self.measurement.
run(diaSources, difference, science, matchedTemplate)
1160 if self.config.doApCorr:
1161 apCorrMap = difference.getInfo().getApCorrMap()
1162 if apCorrMap
is None:
1163 self.log.warning(
"Difference image does not have valid aperture correction; skipping.")
1165 self.applyApCorr.
run(
1167 apCorrMap=apCorrMap,
1170 def measureForcedSources(self, diaSources, image, wcs, template=False):
1171 """Perform forced measurement of the diaSources on a direct image.
1175 diaSources : `lsst.afw.table.SourceCatalog`
1176 The catalog of detected sources.
1177 image: `lsst.afw.image.ExposureF`
1178 Exposure that the forced measurement is being performed on
1179 wcs : `lsst.afw.geom.SkyWcs`
1180 Coordinate system definition (wcs) for the exposure.
1182 Is the forced measurement being performed on the template?
1186 forcedSources = self.forcedMeasurement.generateMeasCat(image, diaSources, wcs)
1187 self.forcedMeasurement.
run(forcedSources, image, diaSources, wcs)
1190 base_key =
'ip_diffim_forced_template_PsfFlux'
1192 base_key =
'ip_diffim_forced_PsfFlux'
1193 mapper = afwTable.SchemaMapper(forcedSources.schema, diaSources.schema)
1194 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFlux")[0],
1195 f
"{base_key}_instFlux",
True)
1196 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFluxErr")[0],
1197 f
"{base_key}_instFluxErr",
True)
1198 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_area")[0],
1199 f
"{base_key}_area",
True)
1200 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag")[0],
1201 f
"{base_key}_flag",
True)
1202 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_noGoodPixels")[0],
1203 f
"{base_key}_flag_noGoodPixels",
True)
1204 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_edge")[0],
1205 f
"{base_key}_flag_edge",
True)
1206 for diaSource, forcedSource
in zip(diaSources, forcedSources):
1207 diaSource.assign(forcedSource, mapper)
1209 def findAndMaskCosmicRays(self, difference):
1210 """Detect and mask cosmic rays on the difference image.
1214 difference : `lsst.afw.image.Exposure`
1215 The background-subtracted difference image.
1216 The mask plane will be modified in place.
1222 crs = findCosmicRays(
1223 difference.maskedImage,
1226 pexConfig.makePropertySet(self.config.cosmicray),
1227 self.config.cosmicray.keepCRs,
1233 mask = difference.maskedImage.mask
1234 crBit = mask.getPlaneBitMask(
"CR")
1235 afwDetection.setMaskFromFootprintList(mask, crs, crBit)
1238 text =
"masked" if self.config.cosmicray.keepCRs
else "interpolated over"
1239 self.log.info(
"Identified and %s %s cosmic rays.", text, num)
1240 self.metadata[
"cosmic_ray_count"] = num
1242 def calculateMetrics(self, science, difference, diaSources, kernelSources):
1243 """Add difference image QA metrics to the Task metadata.
1245 This may be used to produce corresponding metrics (see
1246 lsst.analysis.tools.tasks.diffimTaskDetectorVisitMetricAnalysis).
1250 science : `lsst.afw.image.ExposureF`
1251 Science exposure that was subtracted.
1252 difference : `lsst.afw.image.Exposure`
1253 The target difference image to calculate metrics for.
1254 diaSources : `lsst.afw.table.SourceCatalog`
1255 The catalog of detected sources.
1256 kernelSources : `lsst.afw.table.SourceCatalog`
1257 Final selection of sources that was used for psf matching.
1259 mask = difference.mask
1260 badPix = (mask.array & mask.getPlaneBitMask(self.config.detection.excludeMaskPlanes)) > 0
1261 self.metadata[
"nGoodPixels"] = np.sum(~badPix)
1262 self.metadata[
"nBadPixels"] = np.sum(badPix)
1263 detPosPix = (mask.array & mask.getPlaneBitMask(
"DETECTED")) > 0
1264 detNegPix = (mask.array & mask.getPlaneBitMask(
"DETECTED_NEGATIVE")) > 0
1265 self.metadata[
"nPixelsDetectedPositive"] = np.sum(detPosPix)
1266 self.metadata[
"nPixelsDetectedNegative"] = np.sum(detNegPix)
1269 self.metadata[
"nBadPixelsDetectedPositive"] = np.sum(detPosPix)
1270 self.metadata[
"nBadPixelsDetectedNegative"] = np.sum(detNegPix)
1272 metricsMaskPlanes = list(mask.getMaskPlaneDict().keys())
1273 for maskPlane
in metricsMaskPlanes:
1275 self.metadata[
"%s_mask_fraction"%maskPlane.lower()] = evaluateMaskFraction(mask, maskPlane)
1276 except InvalidParameterError:
1277 self.metadata[
"%s_mask_fraction"%maskPlane.lower()] = -1
1278 self.log.info(
"Unable to calculate metrics for mask plane %s: not in image"%maskPlane)
1280 if self.config.doSkySources:
1281 skySources = diaSources[diaSources[
"sky_source"]]
1284 metrics = computeDifferenceImageMetrics(science, difference, kernelSources, sky_sources=skySources)
1286 self.metadata[
"residualFootprintRatioMean"] = metrics.differenceFootprintRatioMean
1287 self.metadata[
"residualFootprintRatioStdev"] = metrics.differenceFootprintRatioStdev
1288 self.metadata[
"differenceFootprintSkyRatioMean"] = metrics.differenceFootprintSkyRatioMean
1289 self.metadata[
"differenceFootprintSkyRatioStdev"] = metrics.differenceFootprintSkyRatioStdev
1290 self.log.info(
"Mean, stdev of ratio of difference to science "
1291 "pixels in star footprints: %5.4f, %5.4f",
1292 self.metadata[
"residualFootprintRatioMean"],
1293 self.metadata[
"residualFootprintRatioStdev"])
1294 if self.config.raiseOnBadSubtractionRatio:
1295 if metrics.differenceFootprintRatioMean > self.config.badSubtractionRatioThreshold:
1297 threshold=self.config.badSubtractionRatioThreshold)
1298 if metrics.differenceFootprintRatioStdev > self.config.badSubtractionVariationThreshold:
1300 threshold=self.config.badSubtractionVariationThreshold)
1302 def getSattleDiaSourceAllowlist(self, diaSources, science):
1303 """Query the sattle service and determine which diaSources are allowed.
1307 diaSources : `lsst.afw.table.SourceCatalog`
1308 The catalog of detected sources.
1309 science : `lsst.afw.image.ExposureF`
1310 Science exposure that was subtracted.
1314 allow_list : `list` of `int`
1315 diaSourceIds of diaSources that can be made public.
1320 Raised if sattle call does not return success.
1322 wcs = science.getWcs()
1323 visit_info = science.getInfo().getVisitInfo()
1324 visit_id = visit_info.getId()
1325 sattle_uri_base = os.getenv(
'SATTLE_URI_BASE')
1327 dia_sources_json = []
1328 for source
in diaSources:
1329 source_bbox = source.getFootprint().getBBox()
1330 corners = wcs.pixelToSky([
lsst.geom.Point2D(c)
for c
in source_bbox.getCorners()])
1331 bbox_radec = [[pt.getRa().asDegrees(), pt.getDec().asDegrees()]
for pt
in corners]
1332 dia_sources_json.append({
"diasource_id": source[
"id"],
"bbox": bbox_radec})
1334 payload = {
"visit_id": visit_id,
"detector_id": science.getDetector().getId(),
1335 "diasources": dia_sources_json,
"historical": self.config.sattle_historical}
1337 sattle_output = requests.put(f
'{sattle_uri_base}/diasource_allow_list',
1341 if sattle_output.status_code == 404:
1342 self.log.warning(f
'Visit {visit_id} not found in sattle cache, re-sending')
1343 populate_sattle_visit_cache(visit_info, historical=self.config.sattle_historical)
1344 sattle_output = requests.put(f
'{sattle_uri_base}/diasource_allow_list', json=payload)
1346 sattle_output.raise_for_status()
1348 return sattle_output.json()[
'allow_list']
1350 def filterSatellites(self, diaSources, science):
1351 """Remove diaSources overlapping predicted satellite positions.
1355 diaSources : `lsst.afw.table.SourceCatalog`
1356 The catalog of detected sources.
1357 science : `lsst.afw.image.ExposureF`
1358 Science exposure that was subtracted.
1362 filterdDiaSources : `lsst.afw.table.SourceCatalog`
1363 Filtered catalog of diaSources
1366 allow_list = self.getSattleDiaSourceAllowlist(diaSources, science)
1369 allow_set = set(allow_list)
1370 allowed_ids = [source[
'id']
in allow_set
for source
in diaSources]
1371 diaSources = diaSources[np.array(allowed_ids)].copy(deep=
True)
1373 self.log.warning(
'Sattle allowlist is empty, all diaSources removed')
1374 diaSources = diaSources[0:0].copy(deep=
True)
1377 def _runStreakMasking(self, difference):
1378 """Do streak masking and optionally save the resulting streak
1379 fit parameters in a catalog.
1381 Only returns non-empty streakInfo if self.config.writeStreakInfo
1382 is set. The difference image is binned by self.config.streakBinFactor
1383 (and detection is run a second time) so that regions with lower
1384 surface brightness streaks are more likely to fall above the
1385 detection threshold.
1389 difference: `lsst.afw.image.Exposure`
1390 The exposure in which to search for streaks. Must have a detection
1395 streakInfo: `lsst.pipe.base.Struct`
1396 ``rho`` : `np.ndarray`
1397 Angle of detected streak.
1398 ``theta`` : `np.ndarray`
1399 Distance from center of detected streak.
1400 ``sigma`` : `np.ndarray`
1401 Width of streak profile.
1402 ``reducedChi2`` : `np.ndarray`
1403 Reduced chi2 of the best-fit streak profile.
1404 ``modelMaximum`` : `np.ndarray`
1405 Peak value of the fit line profile.
1407 maskedImage = difference.maskedImage
1410 self.config.streakBinFactor,
1411 self.config.streakBinFactor)
1412 binnedExposure = afwImage.ExposureF(binnedMaskedImage.getBBox())
1413 binnedExposure.setMaskedImage(binnedMaskedImage)
1415 binnedExposure.mask &= ~binnedExposure.mask.getPlaneBitMask(
'DETECTED')
1417 sigma = difference.psf.computeShape(difference.psf.getAveragePosition()).getDeterminantRadius()
1418 _table = afwTable.SourceTable.make(afwTable.SourceTable.makeMinimalSchema())
1419 self.streakDetection.
run(table=_table, exposure=binnedExposure, doSmooth=
True,
1420 sigma=sigma/self.config.streakBinFactor)
1421 binnedDetectedMaskPlane = binnedExposure.mask.array & binnedExposure.mask.getPlaneBitMask(
'DETECTED')
1422 rescaledDetectedMaskPlane = binnedDetectedMaskPlane.repeat(self.config.streakBinFactor,
1423 axis=0).repeat(self.config.streakBinFactor,
1426 streakMaskedImage = maskedImage.clone()
1427 ysize, xsize = rescaledDetectedMaskPlane.shape
1428 streakMaskedImage.mask.array[:ysize, :xsize] |= rescaledDetectedMaskPlane
1430 streaks = self.maskStreaks.
run(streakMaskedImage)
1431 streakMaskPlane = streakMaskedImage.mask.array & streakMaskedImage.mask.getPlaneBitMask(
'STREAK')
1433 maskedImage.mask.array |= streakMaskPlane
1435 if self.config.writeStreakInfo:
1436 rhos = np.array([line.rho
for line
in streaks.lines])
1437 thetas = np.array([line.theta
for line
in streaks.lines])
1438 sigmas = np.array([line.sigma
for line
in streaks.lines])
1439 chi2s = np.array([line.reducedChi2
for line
in streaks.lines])
1440 modelMaximums = np.array([line.modelMaximum
for line
in streaks.lines])
1441 streakInfo = {
'rho': rhos,
'theta': thetas,
'sigma': sigmas,
'reducedChi2': chi2s,
1442 'modelMaximum': modelMaximums}
1444 streakInfo = {
'rho': np.array([]),
'theta': np.array([]),
'sigma': np.array([]),
1445 'reducedChi2': np.array([]),
'modelMaximum': np.array([])}
1446 return pipeBase.Struct(maskedStreaks=streakInfo)
1450 scoreExposure = pipeBase.connectionTypes.Input(
1451 doc=
"Maximum likelihood image for detection.",
1452 dimensions=(
"instrument",
"visit",
"detector"),
1453 storageClass=
"ExposureF",
1454 name=
"{fakesType}{coaddName}Diff_scoreTempExp",
1456 scoreMeasuredExposure = pipeBase.connectionTypes.Output(
1457 doc=
"Score image after backgrond subtraction and detection.",
1458 dimensions=(
"instrument",
"visit",
"detector"),
1459 storageClass=
"ExposureF",
1460 name=
"{fakesType}{coaddName}Diff_scoreExp",
1464class DetectAndMeasureScoreConfig(DetectAndMeasureConfig,
1465 pipelineConnections=DetectAndMeasureScoreConnections):
1469class DetectAndMeasureScoreTask(DetectAndMeasureTask):
1470 """Detect DIA sources using a score image,
1471 and measure the detections on the difference image.
1473 Source detection is run on the supplied score, or maximum likelihood,
1474 image. Note that no additional convolution will be done in this case.
1475 Close positive and negative detections will optionally be merged into
1477 Sky sources, or forced detections in background regions, will optionally
1478 be added, and the configured measurement algorithm will be run on all
1481 ConfigClass = DetectAndMeasureScoreConfig
1482 _DefaultName =
"detectAndMeasureScore"
1485 def run(self, science, matchedTemplate, difference, scoreExposure, kernelSources,
1486 idFactory=None, measurementResults=None):
1487 """Detect and measure sources on a score image.
1491 science : `lsst.afw.image.ExposureF`
1492 Science exposure that the template was subtracted from.
1493 matchedTemplate : `lsst.afw.image.ExposureF`
1494 Warped and PSF-matched template that was used produce the
1496 difference : `lsst.afw.image.ExposureF`
1497 Result of subtracting template from the science image.
1498 scoreExposure : `lsst.afw.image.ExposureF`
1499 Score or maximum likelihood difference image
1500 kernelSources : `lsst.afw.table.SourceCatalog`
1501 Final selection of sources that was used for psf matching.
1502 idFactory : `lsst.afw.table.IdFactory`, optional
1503 Generator object used to assign ids to detected sources in the
1504 difference image. Ids from this generator are not set until after
1505 deblending and merging positive/negative peaks.
1506 measurementResults : `lsst.pipe.base.Struct`, optional
1507 Result struct that is modified to allow saving of partial outputs
1508 for some failure conditions. If the task completes successfully,
1509 this is also returned.
1513 measurementResults : `lsst.pipe.base.Struct`
1515 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
1516 Subtracted exposure with detection mask applied.
1517 ``scoreMeasuredExposure`` : `lsst.afw.image.ExposureF`
1518 Score exposure with detection mask applied.
1519 ``diaSources`` : `lsst.afw.table.SourceCatalog`
1520 The catalog of detected sources.
1521 ``differenceBackground`` : `lsst.afw.math.BackgroundList`
1522 Background that was subtracted from the difference image.
1524 if measurementResults
is None:
1525 measurementResults = pipeBase.Struct()
1526 if idFactory
is None:
1527 idFactory = lsst.meas.base.IdGenerator().make_table_id_factory()
1530 self._prepareInputs(difference)
1531 self._prepareInputs(scoreExposure)
1532 if self.config.doSubtractBackground:
1533 background = self._fitAndSubtractBackground(
1534 differenceExposure=difference, scoreExposure=scoreExposure,
1537 background = afwMath.BackgroundList()
1539 if self.config.doFindCosmicRays:
1542 self.findAndMaskCosmicRays(difference)
1543 scoreExposure.mask.array |= difference.mask.array
1548 table = afwTable.SourceTable.make(self.schema)
1549 detectResults = self.detection.
run(
1551 exposure=scoreExposure,
1553 background=background,
1556 measurementResults.differenceBackground = background
1557 measurementResults.scoreMeasuredExposure = scoreExposure
1562 detectedBit = difference.mask.getPlaneBitMask([
"DETECTED"])
1563 detectedPix = scoreExposure.mask.array & detectedBit > 0
1564 detectedNegativeBit = difference.mask.getPlaneBitMask([
"DETECTED_NEGATIVE"])
1565 detectedNegativePix = scoreExposure.mask.array & detectedNegativeBit > 0
1566 difference.mask.array[detectedPix] |= detectedBit
1567 difference.mask.array[detectedNegativePix] |= detectedNegativeBit
1569 edgeBit = difference.mask.getPlaneBitMask([
"EDGE"])
1570 edgePix = scoreExposure.mask.array & edgeBit > 0
1571 difference.mask.array[edgePix] |= edgeBit
1573 if self.config.doDeblend:
1574 sources, positives, negatives = self._deblend(difference,
1575 detectResults.positive,
1576 detectResults.negative)
1578 self.processResults(science, matchedTemplate, difference,
1579 sources, idFactory, kernelSources,
1580 positives=positives,
1581 negatives=negatives,
1582 measurementResults=measurementResults)
1585 positives = afwTable.SourceCatalog(self.schema)
1586 detectResults.positive.makeSources(positives)
1587 negatives = afwTable.SourceCatalog(self.schema)
1588 detectResults.negative.makeSources(negatives)
1589 self.processResults(science, matchedTemplate, difference,
1590 detectResults.sources, idFactory, kernelSources,
1591 positives=positives,
1592 negatives=negatives,
1593 measurementResults=measurementResults)
1594 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)