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_edge",
335 "base_PixelFlags_flag_interpolatedCenterAll",
336 "base_PixelFlags_flag_badCenter",
337 "base_PixelFlags_flag_edgeCenter",
338 "base_PixelFlags_flag_nodataCenter",
339 "base_PixelFlags_flag_saturatedCenter",
340 "base_PixelFlags_flag_saturated_templateCenter",
341 "base_PixelFlags_flag_spikeCenter",
344 clearMaskPlanes = lsst.pex.config.ListField(
346 doc=
"Mask planes to clear before running detection.",
347 default=(
"DETECTED",
"DETECTED_NEGATIVE",
"NOT_DEBLENDED",
"STREAK"),
349 raiseOnBadSubtractionRatio = pexConfig.Field(
352 doc=
"Raise an error if the ratio of power in detected footprints"
353 " on the difference image to the power in footprints on the science"
354 " image exceeds ``badSubtractionRatioThreshold``",
356 badSubtractionRatioThreshold = pexConfig.Field(
359 doc=
"Maximum ratio of power in footprints on the difference image to"
360 " the same footprints on the science image."
361 "Only used if ``raiseOnBadSubtractionRatio`` is set",
363 badSubtractionVariationThreshold = pexConfig.Field(
366 doc=
"Maximum standard deviation of the ratio of power in footprints on"
367 " the difference image to the same footprints on the science image."
368 "Only used if ``raiseOnBadSubtractionRatio`` is set",
370 raiseOnNoDiaSources = pexConfig.Field(
373 doc=
"Raise an algorithm error if no diaSources are detected.",
375 run_sattle = pexConfig.Field(
378 doc=
"If true, dia source bounding boxes will be sent for verification"
379 "to the sattle service."
381 sattle_historical = pexConfig.Field(
384 doc=
"If re-running a pipeline that requires sattle, this should be set "
385 "to True. This will populate sattle's cache with the historic data "
386 "closest in time to the exposure."
388 idGenerator = DetectorVisitIdGeneratorConfig.make_field()
390 def setDefaults(self):
395 self.subtractInitialBackground.binSize = 8
396 self.subtractInitialBackground.useApprox =
False
397 self.subtractInitialBackground.statisticsProperty =
"MEDIAN"
398 self.subtractInitialBackground.doFilterSuperPixels =
True
399 self.subtractInitialBackground.ignoredPixelMask = [
"BAD",
407 self.subtractFinalBackground.binSize = 40
408 self.subtractFinalBackground.useApprox =
False
409 self.subtractFinalBackground.statisticsProperty =
"MEDIAN"
410 self.subtractFinalBackground.doFilterSuperPixels =
True
411 self.subtractFinalBackground.ignoredPixelMask = [
"BAD",
418 self.cosmicray.keepCRs =
True
420 self.detection.thresholdPolarity =
"both"
421 self.detection.thresholdValue = 5.0
422 self.detection.reEstimateBackground =
False
423 self.detection.thresholdType =
"pixel_stdev"
424 self.detection.excludeMaskPlanes = []
427 self.streakDetection.thresholdType = self.detection.thresholdType
428 self.streakDetection.reEstimateBackground =
False
429 self.streakDetection.excludeMaskPlanes = self.detection.excludeMaskPlanes
430 self.streakDetection.thresholdValue = self.detection.thresholdValue
432 self.streakDetection.thresholdPolarity =
"positive"
434 self.streakDetection.nSigmaToGrow = 0
437 self.maskStreaks.onlyMaskDetected =
False
439 self.maskStreaks.maxStreakWidth = 100
442 self.maskStreaks.maxFitIter = 10
444 self.maskStreaks.nSigmaMask = 2
447 self.maskStreaks.absMinimumKernelHeight = 2
449 self.measurement.plugins.names |= [
"ext_trailedSources_Naive",
450 "base_LocalPhotoCalib",
452 "ext_shapeHSM_HsmSourceMoments",
453 "ext_shapeHSM_HsmPsfMoments",
454 "base_ClassificationSizeExtendedness",
456 self.measurement.slots.psfShape =
"ext_shapeHSM_HsmPsfMoments"
457 self.measurement.slots.shape =
"ext_shapeHSM_HsmSourceMoments"
458 self.measurement.plugins[
"base_SdssCentroid"].maxDistToPeak = 5.0
459 self.forcedMeasurement.plugins = [
"base_TransformedCentroid",
"base_PsfFlux"]
460 self.forcedMeasurement.copyColumns = {
461 "id":
"objectId",
"parent":
"parentObjectId",
"coord_ra":
"coord_ra",
"coord_dec":
"coord_dec"}
462 self.forcedMeasurement.slots.centroid =
"base_TransformedCentroid"
463 self.forcedMeasurement.slots.shape =
None
466 self.measurement.plugins[
"base_PixelFlags"].masksFpAnywhere = [
467 "STREAK",
"INJECTED",
"INJECTED_TEMPLATE",
"HIGH_VARIANCE",
"SATURATED_TEMPLATE",
"SPIKE"]
468 self.measurement.plugins[
"base_PixelFlags"].masksFpCenter = [
469 "STREAK",
"INJECTED",
"INJECTED_TEMPLATE",
"HIGH_VARIANCE",
"SATURATED_TEMPLATE",
"SPIKE"]
470 self.skySources.avoidMask = [
"DETECTED",
"DETECTED_NEGATIVE",
"BAD",
"NO_DATA",
"EDGE"]
476 if not os.getenv(
"SATTLE_URI_BASE"):
477 raise pexConfig.FieldValidationError(DetectAndMeasureConfig.run_sattle, self,
478 "Sattle requested but SATTLE_URI_BASE "
479 "environment variable not set.")
482class DetectAndMeasureTask(lsst.pipe.base.PipelineTask):
483 """Detect and measure sources on a difference image.
485 ConfigClass = DetectAndMeasureConfig
486 _DefaultName =
"detectAndMeasure"
488 def __init__(self, **kwargs):
489 super().__init__(**kwargs)
490 self.schema = afwTable.SourceTable.makeMinimalSchema()
493 if self.config.doSubtractBackground:
494 self.makeSubtask(
"subtractInitialBackground")
495 self.makeSubtask(
"subtractFinalBackground")
496 self.makeSubtask(
"detection", schema=self.schema)
497 if self.config.doDeblend:
498 self.makeSubtask(
"deblend", schema=self.schema)
499 self.makeSubtask(
"setPrimaryFlags", schema=self.schema, isSingleFrame=
True)
500 self.makeSubtask(
"measurement", schema=self.schema,
501 algMetadata=self.algMetadata)
502 if self.config.doApCorr:
503 self.makeSubtask(
"applyApCorr", schema=self.measurement.schema)
504 if self.config.doForcedMeasurement:
505 self.schema.addField(
506 "ip_diffim_forced_PsfFlux_instFlux",
"D",
507 "Forced PSF flux measured on the direct image.",
509 self.schema.addField(
510 "ip_diffim_forced_PsfFlux_instFluxErr",
"D",
511 "Forced PSF flux error measured on the direct image.",
513 self.schema.addField(
514 "ip_diffim_forced_PsfFlux_area",
"F",
515 "Forced PSF flux effective area of PSF.",
517 self.schema.addField(
518 "ip_diffim_forced_PsfFlux_flag",
"Flag",
519 "Forced PSF flux general failure flag.")
520 self.schema.addField(
521 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
"Flag",
522 "Forced PSF flux not enough non-rejected pixels in data to attempt the fit.")
523 self.schema.addField(
524 "ip_diffim_forced_PsfFlux_flag_edge",
"Flag",
525 "Forced PSF flux object was too close to the edge of the image to use the full PSF model.")
526 self.schema.addField(
527 "ip_diffim_forced_template_PsfFlux_instFlux",
"D",
528 "Forced PSF flux measured on the template image.",
530 self.schema.addField(
531 "ip_diffim_forced_template_PsfFlux_instFluxErr",
"D",
532 "Forced PSF flux error measured on the template image.",
534 self.schema.addField(
535 "ip_diffim_forced_template_PsfFlux_area",
"F",
536 "Forced template PSF flux effective area of PSF.",
538 self.schema.addField(
539 "ip_diffim_forced_template_PsfFlux_flag",
"Flag",
540 "Forced template PSF flux general failure flag.")
541 self.schema.addField(
542 "ip_diffim_forced_template_PsfFlux_flag_noGoodPixels",
"Flag",
543 "Forced template PSF flux not enough non-rejected pixels in data to attempt the fit.")
544 self.schema.addField(
545 "ip_diffim_forced_template_PsfFlux_flag_edge",
"Flag",
546 """Forced template PSF flux object was too close to the edge of the image """
547 """to use the full PSF model.""")
548 self.makeSubtask(
"forcedMeasurement", refSchema=self.schema)
550 self.schema.addField(
"refMatchId",
"L",
"unique id of reference catalog match")
551 self.schema.addField(
"srcMatchId",
"L",
"unique id of source match")
554 self.makeSubtask(
"skySources", schema=self.schema)
555 if self.config.doMaskStreaks:
556 self.makeSubtask(
"maskStreaks")
557 self.makeSubtask(
"streakDetection")
558 self.makeSubtask(
"findGlints")
559 self.schema.addField(
"glint_trail",
"Flag",
"DiaSource is part of a glint trail.")
560 self.schema.addField(
"reliability", type=
"F", doc=
"Reliability score of the DiaSource")
567 for flag
in self.config.badSourceFlags:
568 if flag
not in self.schema:
569 raise pipeBase.InvalidQuantumError(
"Field %s not in schema" % flag)
572 self.outputSchema = afwTable.SourceCatalog(self.schema)
573 self.outputSchema.getTable().setMetadata(self.algMetadata)
575 def runQuantum(self, butlerQC: pipeBase.QuantumContext,
576 inputRefs: pipeBase.InputQuantizedConnection,
577 outputRefs: pipeBase.OutputQuantizedConnection):
578 inputs = butlerQC.get(inputRefs)
579 idGenerator = self.config.idGenerator.apply(butlerQC.quantum.dataId)
580 idFactory = idGenerator.make_table_id_factory()
583 measurementResults = pipeBase.Struct(
584 subtractedMeasuredExposure=
None,
587 differenceBackground=
None,
590 self.run(**inputs, idFactory=idFactory, measurementResults=measurementResults)
591 except pipeBase.AlgorithmError
as e:
592 error = pipeBase.AnnotatedPartialOutputsError.annotate(
595 measurementResults.subtractedMeasuredExposure,
596 measurementResults.diaSources,
597 measurementResults.maskedStreaks,
600 butlerQC.put(measurementResults, outputRefs)
602 butlerQC.put(measurementResults, outputRefs)
605 def run(self, science, matchedTemplate, difference, kernelSources=None,
606 idFactory=None, measurementResults=None):
607 """Detect and measure sources on a difference image.
609 The difference image will be convolved with a gaussian approximation of
610 the PSF to form a maximum likelihood image for detection.
611 Close positive and negative detections will optionally be merged into
613 Sky sources, or forced detections in background regions, will optionally
614 be added, and the configured measurement algorithm will be run on all
619 science : `lsst.afw.image.ExposureF`
620 Science exposure that the template was subtracted from.
621 matchedTemplate : `lsst.afw.image.ExposureF`
622 Warped and PSF-matched template that was used produce the
624 difference : `lsst.afw.image.ExposureF`
625 Result of subtracting template from the science image.
626 kernelSources : `lsst.afw.table.SourceCatalog`, optional
627 Final selection of sources that was used for psf matching.
628 idFactory : `lsst.afw.table.IdFactory`, optional
629 Generator object used to assign ids to detected sources in the
630 difference image. Ids from this generator are not set until after
631 deblending and merging positive/negative peaks.
632 measurementResults : `lsst.pipe.base.Struct`, optional
633 Result struct that is modified to allow saving of partial outputs
634 for some failure conditions. If the task completes successfully,
635 this is also returned.
639 measurementResults : `lsst.pipe.base.Struct`
641 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
642 Subtracted exposure with detection mask applied.
643 ``diaSources`` : `lsst.afw.table.SourceCatalog`
644 The catalog of detected sources.
645 ``differenceBackground`` : `lsst.afw.math.BackgroundList`
646 Background that was subtracted from the difference image.
648 if measurementResults
is None:
649 measurementResults = pipeBase.Struct()
650 if idFactory
is None:
651 idFactory = lsst.meas.base.IdGenerator().make_table_id_factory()
653 if self.config.doSubtractBackground:
655 detectionExposure = difference.clone()
656 background = self.subtractInitialBackground.
run(detectionExposure).background
658 detectionExposure = difference
659 background = afwMath.BackgroundList()
661 self._prepareInputs(detectionExposure)
663 if self.config.doFindCosmicRays
and not self.config.doSubtractBackground:
666 self.findAndMaskCosmicRays(detectionExposure)
671 table = afwTable.SourceTable.make(self.schema)
672 results = self.detection.
run(
674 exposure=detectionExposure,
676 background=background
679 if self.config.doSubtractBackground:
683 difference.setMask(detectionExposure.mask)
684 background = self.subtractFinalBackground.
run(difference).background
686 if self.config.doFindCosmicRays:
688 self.findAndMaskCosmicRays(difference)
691 table = afwTable.SourceTable.make(self.schema)
692 results = self.detection.
run(
696 background=background
698 measurementResults.differenceBackground = background
700 if self.config.doDeblend:
701 sources, positives, negatives = self._deblend(difference,
706 positives = afwTable.SourceCatalog(self.schema)
707 results.positive.makeSources(positives)
708 negatives = afwTable.SourceCatalog(self.schema)
709 results.negative.makeSources(negatives)
710 sources = results.sources
712 self.processResults(science, matchedTemplate, difference,
713 sources, idFactory, kernelSources,
716 measurementResults=measurementResults)
717 return measurementResults
719 def _prepareInputs(self, difference):
720 """Ensure that we start with an empty detection and deblended mask.
724 difference : `lsst.afw.image.ExposureF`
725 The difference image that will be used for detecting diaSources.
726 The mask plane will be modified in place.
730 lsst.pipe.base.UpstreamFailureNoWorkFound
731 If the PSF is not usable for measurement.
734 sigma = difference.psf.computeShape(difference.psf.getAveragePosition()).getDeterminantRadius()
736 raise pipeBase.UpstreamFailureNoWorkFound(
"Invalid PSF detected! PSF width evaluates to NaN.")
738 mask = difference.mask
739 for mp
in self.config.clearMaskPlanes:
740 if mp
not in mask.getMaskPlaneDict():
741 mask.addMaskPlane(mp)
742 mask &= ~mask.getPlaneBitMask(self.config.clearMaskPlanes)
744 def processResults(self, science, matchedTemplate, difference, sources, idFactory,
745 kernelSources=None, positives=None, negatives=None, measurementResults=None):
746 """Measure and process the results of source detection.
750 science : `lsst.afw.image.ExposureF`
751 Science exposure that the template was subtracted from.
752 matchedTemplate : `lsst.afw.image.ExposureF`
753 Warped and PSF-matched template that was used produce the
755 difference : `lsst.afw.image.ExposureF`
756 Result of subtracting template from the science image.
757 sources : `lsst.afw.table.SourceCatalog`
758 Detected sources on the difference exposure.
759 idFactory : `lsst.afw.table.IdFactory`
760 Generator object used to assign ids to detected sources in the
762 kernelSources : `lsst.afw.table.SourceCatalog`, optional
763 Final selection of sources that was used for psf matching.
764 positives : `lsst.afw.table.SourceCatalog`, optional
765 Positive polarity footprints.
766 negatives : `lsst.afw.table.SourceCatalog`, optional
767 Negative polarity footprints.
768 measurementResults : `lsst.pipe.base.Struct`, optional
769 Result struct that is modified to allow saving of partial outputs
770 for some failure conditions. If the task completes successfully,
771 this is also returned.
775 measurementResults : `lsst.pipe.base.Struct`
777 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
778 Subtracted exposure with detection mask applied.
779 ``diaSources`` : `lsst.afw.table.SourceCatalog`
780 The catalog of detected sources.
782 if measurementResults
is None:
783 measurementResults = pipeBase.Struct()
784 self.metadata[
"nUnmergedDiaSources"] = len(sources)
785 if self.config.doMerge:
787 if len(positives) > 0:
788 peakSchema = positives[0].getFootprint().peaks.schema
789 elif len(negatives) > 0:
790 peakSchema = negatives[0].getFootprint().peaks.schema
792 peakSchema = afwDetection.PeakTable.makeMinimalSchema()
793 mergeList = afwDetection.FootprintMergeList(self.schema,
794 [
"positive",
"negative"], peakSchema)
795 initialDiaSources = afwTable.SourceCatalog(self.schema)
799 mergeList.addCatalog(initialDiaSources.table, positives,
"positive", minNewPeakDist=0)
800 mergeList.addCatalog(initialDiaSources.table, negatives,
"negative", minNewPeakDist=0)
801 mergeList.getFinalSources(initialDiaSources)
804 initialDiaSources[
"is_negative"] = initialDiaSources[
"merge_footprint_negative"] & \
805 ~initialDiaSources[
"merge_footprint_positive"]
806 self.log.info(
"Merging detections into %d sources", len(initialDiaSources))
808 initialDiaSources = sources
812 for source
in initialDiaSources:
815 initialDiaSources.getTable().setIdFactory(idFactory)
816 initialDiaSources.setMetadata(self.algMetadata)
818 self.metadata[
"nMergedDiaSources"] = len(initialDiaSources)
820 if self.config.doMaskStreaks:
821 streakInfo = self._runStreakMasking(difference)
823 if self.config.doSkySources:
824 self.addSkySources(initialDiaSources, difference.mask, difference.info.id)
826 if not initialDiaSources.isContiguous():
827 initialDiaSources = initialDiaSources.copy(deep=
True)
829 self.measureDiaSources(initialDiaSources, science, difference, matchedTemplate)
832 diaSources = self._removeBadSources(initialDiaSources)
834 if self.config.run_sattle:
835 diaSources = self.filterSatellites(diaSources, science)
838 diaSources, trail_parameters = self._find_glint_trails(diaSources)
839 if self.config.writeGlintInfo:
840 measurementResults.mergeItems(trail_parameters,
'glintTrailInfo')
842 if self.config.doForcedMeasurement:
843 self.measureForcedSources(diaSources, science, difference.getWcs())
844 self.measureForcedSources(diaSources, matchedTemplate, difference.getWcs(),
851 difference.image.array[difference.mask.array & difference.mask.getPlaneBitMask(
'NO_DATA') > 0] = 0
853 measurementResults.subtractedMeasuredExposure = difference
855 if self.config.doMaskStreaks
and self.config.writeStreakInfo:
856 measurementResults.maskedStreaks = streakInfo.maskedStreaks
858 if kernelSources
is not None:
859 self.calculateMetrics(science, difference, diaSources, kernelSources)
861 if np.count_nonzero(~diaSources[
"sky_source"]) > 0:
862 measurementResults.diaSources = diaSources
863 elif self.config.raiseOnNoDiaSources:
865 elif len(diaSources) > 0:
868 measurementResults.diaSources = diaSources
869 self.log.info(
"Measured %d diaSources and %d sky sources",
870 np.count_nonzero(~diaSources[
"sky_source"]),
871 np.count_nonzero(diaSources[
"sky_source"])
873 return measurementResults
875 def _deblend(self, difference, positiveFootprints, negativeFootprints):
876 """Deblend the positive and negative footprints and return a catalog
877 containing just the children, and the deblended footprints.
881 difference : `lsst.afw.image.Exposure`
882 Result of subtracting template from the science image.
883 positiveFootprints, negativeFootprints : `lsst.afw.detection.FootprintSet`
884 Positive and negative polarity footprints measured on
885 ``difference`` to be deblended separately.
889 sources : `lsst.afw.table.SourceCatalog`
890 Positive and negative deblended children.
891 positives, negatives : `lsst.afw.table.SourceCatalog`
892 Deblended positive and negative polarity sources with footprints
893 detected on ``difference``.
896 def deblend(footprints, negative=False):
897 """Deblend a positive or negative footprint set,
898 and return the deblended children.
902 footprints : `lsst.afw.detection.FootprintSet`
904 Set True if the footprints contain negative fluxes
908 sources : `lsst.afw.table.SourceCatalog`
910 sources = afwTable.SourceCatalog(self.schema)
911 footprints.makeSources(sources)
914 difference_inverted = difference.clone()
915 difference_inverted.image *= -1
916 self.deblend.
run(exposure=difference_inverted, sources=sources)
917 children = sources[sources[
"parent"] != 0]
919 for child
in children:
920 footprint = child.getFootprint()
921 array = footprint.getImageArray()
924 self.deblend.
run(exposure=difference, sources=sources)
925 self.setPrimaryFlags.
run(sources)
926 children = sources[
"detect_isDeblendedSource"] == 1
927 sources = sources[children].copy(deep=
True)
929 sources[
'parent'] = 0
930 return sources.copy(deep=
True)
932 positives = deblend(positiveFootprints)
933 negatives = deblend(negativeFootprints, negative=
True)
935 sources = afwTable.SourceCatalog(self.schema)
936 sources.reserve(len(positives) + len(negatives))
937 sources.extend(positives, deep=
True)
938 sources.extend(negatives, deep=
True)
939 if len(negatives) > 0:
940 sources[-len(negatives):][
"is_negative"] =
True
941 return sources, positives, negatives
943 def _removeBadSources(self, diaSources):
944 """Remove unphysical diaSources from the catalog.
948 diaSources : `lsst.afw.table.SourceCatalog`
949 The catalog of detected sources.
953 diaSources : `lsst.afw.table.SourceCatalog`
954 The updated catalog of detected sources, with any source that has a
955 flag in ``config.badSourceFlags`` set removed.
957 selector = np.ones(len(diaSources), dtype=bool)
958 for flag
in self.config.badSourceFlags:
959 flags = diaSources[flag]
960 nBad = np.count_nonzero(flags)
962 self.log.debug(
"Found %d unphysical sources with flag %s.", nBad, flag)
964 nBadTotal = np.count_nonzero(~selector)
965 self.metadata[
"nRemovedBadFlaggedSources"] = nBadTotal
966 self.log.info(
"Removed %d unphysical sources.", nBadTotal)
967 return diaSources[selector].copy(deep=
True)
969 def _find_glint_trails(self, diaSources):
970 """Define a new flag column for diaSources that are in a glint trail.
974 diaSources : `lsst.afw.table.SourceCatalog`
975 The catalog of detected sources.
979 diaSources : `lsst.afw.table.SourceCatalog`
980 The updated catalog of detected sources, with a new bool column
981 called 'glint_trail' added.
983 trail_parameters : `dict`
984 Parameters of all the trails that were found.
986 if self.config.doSkySources:
988 candidateDiaSources = diaSources[~diaSources[
"sky_source"]].copy(deep=
True)
990 candidateDiaSources = diaSources
991 trailed_glints = self.findGlints.
run(candidateDiaSources)
992 glint_mask = [
True if id
in trailed_glints.trailed_ids
else False for id
in diaSources[
'id']]
993 if np.any(glint_mask):
994 diaSources[
'glint_trail'] = np.array(glint_mask)
996 slopes = np.array([trail.slope
for trail
in trailed_glints.parameters])
997 intercepts = np.array([trail.intercept
for trail
in trailed_glints.parameters])
998 stderrs = np.array([trail.stderr
for trail
in trailed_glints.parameters])
999 lengths = np.array([trail.length
for trail
in trailed_glints.parameters])
1000 angles = np.array([trail.angle
for trail
in trailed_glints.parameters])
1001 parameters = {
'slopes': slopes,
'intercepts': intercepts,
'stderrs': stderrs,
'lengths': lengths,
1004 trail_parameters = pipeBase.Struct(glintTrailInfo=parameters)
1006 return diaSources, trail_parameters
1008 def addSkySources(self, diaSources, mask, seed,
1010 """Add sources in empty regions of the difference image
1011 for measuring the background.
1015 diaSources : `lsst.afw.table.SourceCatalog`
1016 The catalog of detected sources.
1017 mask : `lsst.afw.image.Mask`
1018 Mask plane for determining regions where Sky sources can be added.
1020 Seed value to initialize the random number generator.
1023 subtask = self.skySources
1024 if subtask.config.nSources <= 0:
1025 self.metadata[f
"n_{subtask.getName()}"] = 0
1027 skySourceFootprints = subtask.run(mask=mask, seed=seed, catalog=diaSources)
1028 self.metadata[f
"n_{subtask.getName()}"] = len(skySourceFootprints)
1030 def measureDiaSources(self, diaSources, science, difference, matchedTemplate):
1031 """Use (matched) template and science image to constrain dipole fitting.
1035 diaSources : `lsst.afw.table.SourceCatalog`
1036 The catalog of detected sources.
1037 science : `lsst.afw.image.ExposureF`
1038 Science exposure that the template was subtracted from.
1039 difference : `lsst.afw.image.ExposureF`
1040 Result of subtracting template from the science image.
1041 matchedTemplate : `lsst.afw.image.ExposureF`
1042 Warped and PSF-matched template that was used produce the
1046 for mp
in self.config.measurement.plugins[
"base_PixelFlags"].masksFpAnywhere:
1047 difference.mask.addMaskPlane(mp)
1050 self.measurement.
run(diaSources, difference, science, matchedTemplate)
1051 if self.config.doApCorr:
1052 apCorrMap = difference.getInfo().getApCorrMap()
1053 if apCorrMap
is None:
1054 self.log.warning(
"Difference image does not have valid aperture correction; skipping.")
1056 self.applyApCorr.
run(
1058 apCorrMap=apCorrMap,
1061 def measureForcedSources(self, diaSources, image, wcs, template=False):
1062 """Perform forced measurement of the diaSources on a direct image.
1066 diaSources : `lsst.afw.table.SourceCatalog`
1067 The catalog of detected sources.
1068 image: `lsst.afw.image.ExposureF`
1069 Exposure that the forced measurement is being performed on
1070 wcs : `lsst.afw.geom.SkyWcs`
1071 Coordinate system definition (wcs) for the exposure.
1073 Is the forced measurement being performed on the template?
1077 forcedSources = self.forcedMeasurement.generateMeasCat(image, diaSources, wcs)
1078 self.forcedMeasurement.
run(forcedSources, image, diaSources, wcs)
1081 base_key =
'ip_diffim_forced_template_PsfFlux'
1083 base_key =
'ip_diffim_forced_PsfFlux'
1084 mapper = afwTable.SchemaMapper(forcedSources.schema, diaSources.schema)
1085 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFlux")[0],
1086 f
"{base_key}_instFlux",
True)
1087 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFluxErr")[0],
1088 f
"{base_key}_instFluxErr",
True)
1089 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_area")[0],
1090 f
"{base_key}_area",
True)
1091 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag")[0],
1092 f
"{base_key}_flag",
True)
1093 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_noGoodPixels")[0],
1094 f
"{base_key}_flag_noGoodPixels",
True)
1095 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_edge")[0],
1096 f
"{base_key}_flag_edge",
True)
1097 for diaSource, forcedSource
in zip(diaSources, forcedSources):
1098 diaSource.assign(forcedSource, mapper)
1100 def findAndMaskCosmicRays(self, difference):
1101 """Detect and mask cosmic rays on the difference image.
1105 difference : `lsst.afw.image.Exposure`
1106 The background-subtracted difference image.
1107 The mask plane will be modified in place.
1113 crs = findCosmicRays(
1114 difference.maskedImage,
1117 pexConfig.makePropertySet(self.config.cosmicray),
1118 self.config.cosmicray.keepCRs,
1124 mask = difference.maskedImage.mask
1125 crBit = mask.getPlaneBitMask(
"CR")
1126 afwDetection.setMaskFromFootprintList(mask, crs, crBit)
1129 text =
"masked" if self.config.cosmicray.keepCRs
else "interpolated over"
1130 self.log.info(
"Identified and %s %s cosmic rays.", text, num)
1131 self.metadata[
"cosmic_ray_count"] = num
1133 def calculateMetrics(self, science, difference, diaSources, kernelSources):
1134 """Add difference image QA metrics to the Task metadata.
1136 This may be used to produce corresponding metrics (see
1137 lsst.analysis.tools.tasks.diffimTaskDetectorVisitMetricAnalysis).
1141 science : `lsst.afw.image.ExposureF`
1142 Science exposure that was subtracted.
1143 difference : `lsst.afw.image.Exposure`
1144 The target difference image to calculate metrics for.
1145 diaSources : `lsst.afw.table.SourceCatalog`
1146 The catalog of detected sources.
1147 kernelSources : `lsst.afw.table.SourceCatalog`
1148 Final selection of sources that was used for psf matching.
1150 mask = difference.mask
1151 badPix = (mask.array & mask.getPlaneBitMask(self.config.detection.excludeMaskPlanes)) > 0
1152 self.metadata[
"nGoodPixels"] = np.sum(~badPix)
1153 self.metadata[
"nBadPixels"] = np.sum(badPix)
1154 detPosPix = (mask.array & mask.getPlaneBitMask(
"DETECTED")) > 0
1155 detNegPix = (mask.array & mask.getPlaneBitMask(
"DETECTED_NEGATIVE")) > 0
1156 self.metadata[
"nPixelsDetectedPositive"] = np.sum(detPosPix)
1157 self.metadata[
"nPixelsDetectedNegative"] = np.sum(detNegPix)
1160 self.metadata[
"nBadPixelsDetectedPositive"] = np.sum(detPosPix)
1161 self.metadata[
"nBadPixelsDetectedNegative"] = np.sum(detNegPix)
1163 metricsMaskPlanes = list(mask.getMaskPlaneDict().keys())
1164 for maskPlane
in metricsMaskPlanes:
1166 self.metadata[
"%s_mask_fraction"%maskPlane.lower()] = evaluateMaskFraction(mask, maskPlane)
1167 except InvalidParameterError:
1168 self.metadata[
"%s_mask_fraction"%maskPlane.lower()] = -1
1169 self.log.info(
"Unable to calculate metrics for mask plane %s: not in image"%maskPlane)
1171 if self.config.doSkySources:
1172 skySources = diaSources[diaSources[
"sky_source"]]
1175 metrics = computeDifferenceImageMetrics(science, difference, kernelSources, sky_sources=skySources)
1177 self.metadata[
"residualFootprintRatioMean"] = metrics.differenceFootprintRatioMean
1178 self.metadata[
"residualFootprintRatioStdev"] = metrics.differenceFootprintRatioStdev
1179 self.metadata[
"differenceFootprintSkyRatioMean"] = metrics.differenceFootprintSkyRatioMean
1180 self.metadata[
"differenceFootprintSkyRatioStdev"] = metrics.differenceFootprintSkyRatioStdev
1181 self.log.info(
"Mean, stdev of ratio of difference to science "
1182 "pixels in star footprints: %5.4f, %5.4f",
1183 self.metadata[
"residualFootprintRatioMean"],
1184 self.metadata[
"residualFootprintRatioStdev"])
1185 if self.config.raiseOnBadSubtractionRatio:
1186 if metrics.differenceFootprintRatioMean > self.config.badSubtractionRatioThreshold:
1188 threshold=self.config.badSubtractionRatioThreshold)
1189 if metrics.differenceFootprintRatioStdev > self.config.badSubtractionVariationThreshold:
1191 threshold=self.config.badSubtractionVariationThreshold)
1193 def getSattleDiaSourceAllowlist(self, diaSources, science):
1194 """Query the sattle service and determine which diaSources are allowed.
1198 diaSources : `lsst.afw.table.SourceCatalog`
1199 The catalog of detected sources.
1200 science : `lsst.afw.image.ExposureF`
1201 Science exposure that was subtracted.
1205 allow_list : `list` of `int`
1206 diaSourceIds of diaSources that can be made public.
1211 Raised if sattle call does not return success.
1213 wcs = science.getWcs()
1214 visit_info = science.getInfo().getVisitInfo()
1215 visit_id = visit_info.getId()
1216 sattle_uri_base = os.getenv(
'SATTLE_URI_BASE')
1218 dia_sources_json = []
1219 for source
in diaSources:
1220 source_bbox = source.getFootprint().getBBox()
1221 corners = wcs.pixelToSky([
lsst.geom.Point2D(c)
for c
in source_bbox.getCorners()])
1222 bbox_radec = [[pt.getRa().asDegrees(), pt.getDec().asDegrees()]
for pt
in corners]
1223 dia_sources_json.append({
"diasource_id": source[
"id"],
"bbox": bbox_radec})
1225 payload = {
"visit_id": visit_id,
"detector_id": science.getDetector().getId(),
1226 "diasources": dia_sources_json,
"historical": self.config.sattle_historical}
1228 sattle_output = requests.put(f
'{sattle_uri_base}/diasource_allow_list',
1232 if sattle_output.status_code == 404:
1233 self.log.warning(f
'Visit {visit_id} not found in sattle cache, re-sending')
1234 populate_sattle_visit_cache(visit_info, historical=self.config.sattle_historical)
1235 sattle_output = requests.put(f
'{sattle_uri_base}/diasource_allow_list', json=payload)
1237 sattle_output.raise_for_status()
1239 return sattle_output.json()[
'allow_list']
1241 def filterSatellites(self, diaSources, science):
1242 """Remove diaSources overlapping predicted satellite positions.
1246 diaSources : `lsst.afw.table.SourceCatalog`
1247 The catalog of detected sources.
1248 science : `lsst.afw.image.ExposureF`
1249 Science exposure that was subtracted.
1253 filterdDiaSources : `lsst.afw.table.SourceCatalog`
1254 Filtered catalog of diaSources
1257 allow_list = self.getSattleDiaSourceAllowlist(diaSources, science)
1260 allow_set = set(allow_list)
1261 allowed_ids = [source[
'id']
in allow_set
for source
in diaSources]
1262 diaSources = diaSources[np.array(allowed_ids)].copy(deep=
True)
1264 self.log.warning(
'Sattle allowlist is empty, all diaSources removed')
1265 diaSources = diaSources[0:0].copy(deep=
True)
1268 def _runStreakMasking(self, difference):
1269 """Do streak masking and optionally save the resulting streak
1270 fit parameters in a catalog.
1272 Only returns non-empty streakInfo if self.config.writeStreakInfo
1273 is set. The difference image is binned by self.config.streakBinFactor
1274 (and detection is run a second time) so that regions with lower
1275 surface brightness streaks are more likely to fall above the
1276 detection threshold.
1280 difference: `lsst.afw.image.Exposure`
1281 The exposure in which to search for streaks. Must have a detection
1286 streakInfo: `lsst.pipe.base.Struct`
1287 ``rho`` : `np.ndarray`
1288 Angle of detected streak.
1289 ``theta`` : `np.ndarray`
1290 Distance from center of detected streak.
1291 ``sigma`` : `np.ndarray`
1292 Width of streak profile.
1293 ``reducedChi2`` : `np.ndarray`
1294 Reduced chi2 of the best-fit streak profile.
1295 ``modelMaximum`` : `np.ndarray`
1296 Peak value of the fit line profile.
1298 maskedImage = difference.maskedImage
1301 self.config.streakBinFactor,
1302 self.config.streakBinFactor)
1303 binnedExposure = afwImage.ExposureF(binnedMaskedImage.getBBox())
1304 binnedExposure.setMaskedImage(binnedMaskedImage)
1306 binnedExposure.mask &= ~binnedExposure.mask.getPlaneBitMask(
'DETECTED')
1308 sigma = difference.psf.computeShape(difference.psf.getAveragePosition()).getDeterminantRadius()
1309 _table = afwTable.SourceTable.make(afwTable.SourceTable.makeMinimalSchema())
1310 self.streakDetection.
run(table=_table, exposure=binnedExposure, doSmooth=
True,
1311 sigma=sigma/self.config.streakBinFactor)
1312 binnedDetectedMaskPlane = binnedExposure.mask.array & binnedExposure.mask.getPlaneBitMask(
'DETECTED')
1313 rescaledDetectedMaskPlane = binnedDetectedMaskPlane.repeat(self.config.streakBinFactor,
1314 axis=0).repeat(self.config.streakBinFactor,
1317 streakMaskedImage = maskedImage.clone()
1318 ysize, xsize = rescaledDetectedMaskPlane.shape
1319 streakMaskedImage.mask.array[:ysize, :xsize] |= rescaledDetectedMaskPlane
1321 streaks = self.maskStreaks.
run(streakMaskedImage)
1322 streakMaskPlane = streakMaskedImage.mask.array & streakMaskedImage.mask.getPlaneBitMask(
'STREAK')
1324 maskedImage.mask.array |= streakMaskPlane
1326 if self.config.writeStreakInfo:
1327 rhos = np.array([line.rho
for line
in streaks.lines])
1328 thetas = np.array([line.theta
for line
in streaks.lines])
1329 sigmas = np.array([line.sigma
for line
in streaks.lines])
1330 chi2s = np.array([line.reducedChi2
for line
in streaks.lines])
1331 modelMaximums = np.array([line.modelMaximum
for line
in streaks.lines])
1332 streakInfo = {
'rho': rhos,
'theta': thetas,
'sigma': sigmas,
'reducedChi2': chi2s,
1333 'modelMaximum': modelMaximums}
1335 streakInfo = {
'rho': np.array([]),
'theta': np.array([]),
'sigma': np.array([]),
1336 'reducedChi2': np.array([]),
'modelMaximum': np.array([])}
1337 return pipeBase.Struct(maskedStreaks=streakInfo)
1341 scoreExposure = pipeBase.connectionTypes.Input(
1342 doc=
"Maximum likelihood image for detection.",
1343 dimensions=(
"instrument",
"visit",
"detector"),
1344 storageClass=
"ExposureF",
1345 name=
"{fakesType}{coaddName}Diff_scoreExp",
1349class DetectAndMeasureScoreConfig(DetectAndMeasureConfig,
1350 pipelineConnections=DetectAndMeasureScoreConnections):
1354class DetectAndMeasureScoreTask(DetectAndMeasureTask):
1355 """Detect DIA sources using a score image,
1356 and measure the detections on the difference image.
1358 Source detection is run on the supplied score, or maximum likelihood,
1359 image. Note that no additional convolution will be done in this case.
1360 Close positive and negative detections will optionally be merged into
1362 Sky sources, or forced detections in background regions, will optionally
1363 be added, and the configured measurement algorithm will be run on all
1366 ConfigClass = DetectAndMeasureScoreConfig
1367 _DefaultName =
"detectAndMeasureScore"
1370 def run(self, science, matchedTemplate, difference, scoreExposure, kernelSources,
1372 """Detect and measure sources on a score image.
1376 science : `lsst.afw.image.ExposureF`
1377 Science exposure that the template was subtracted from.
1378 matchedTemplate : `lsst.afw.image.ExposureF`
1379 Warped and PSF-matched template that was used produce the
1381 difference : `lsst.afw.image.ExposureF`
1382 Result of subtracting template from the science image.
1383 scoreExposure : `lsst.afw.image.ExposureF`
1384 Score or maximum likelihood difference image
1385 kernelSources : `lsst.afw.table.SourceCatalog`
1386 Final selection of sources that was used for psf matching.
1387 idFactory : `lsst.afw.table.IdFactory`, optional
1388 Generator object used to assign ids to detected sources in the
1389 difference image. Ids from this generator are not set until after
1390 deblending and merging positive/negative peaks.
1394 measurementResults : `lsst.pipe.base.Struct`
1396 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
1397 Subtracted exposure with detection mask applied.
1398 ``diaSources`` : `lsst.afw.table.SourceCatalog`
1399 The catalog of detected sources.
1401 if idFactory
is None:
1402 idFactory = lsst.meas.base.IdGenerator().make_table_id_factory()
1404 self._prepareInputs(scoreExposure)
1409 table = afwTable.SourceTable.make(self.schema)
1410 results = self.detection.
run(
1412 exposure=scoreExposure,
1416 difference.mask.assign(scoreExposure.mask, scoreExposure.getBBox())
1418 if self.config.doDeblend:
1419 sources, positives, negatives = self._deblend(difference,
1423 return self.processResults(science, matchedTemplate, difference,
1424 sources, idFactory, kernelSources,
1425 positives=positives,
1426 negatives=negatives)
1429 positives = afwTable.SourceCatalog(self.schema)
1430 results.positive.makeSources(positives)
1431 negatives = afwTable.SourceCatalog(self.schema)
1432 results.negative.makeSources(negatives)
1433 return self.processResults(science, matchedTemplate, difference,
1434 results.sources, idFactory, kernelSources,
1435 positives=positives,
1436 negatives=negatives)
__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)