34from lsst.images
import Mask, get_legacy_difference_image_mask_planes
35from lsst.images.fields
import field_from_legacy_background
37 populate_sattle_visit_cache,
38 get_difference_image_provenance)
39from lsst.meas.algorithms
import SkyObjectsTask, SourceDetectionTask, SetPrimaryFlagsTask, MaskStreaksTask
40from lsst.meas.algorithms
import FindGlintTrailsTask, FindCosmicRaysConfig, findCosmicRays
41from lsst.meas.base import ForcedMeasurementTask, ApplyApCorrTask, DetectorVisitIdGeneratorConfig
42import lsst.meas.deblender
43import lsst.meas.extensions.trailedSources
44import lsst.meas.extensions.shapeHSM
49from lsst.utils.timer
import timeMethod
51from .
import DipoleFitTask
53__all__ = [
"DetectAndMeasureConfig",
"DetectAndMeasureTask",
54 "DetectAndMeasureScoreConfig",
"DetectAndMeasureScoreTask"]
58 """Raised when the residuals in footprints of stars used to compute the
59 psf-matching kernel exceeds the configured maximum.
62 msg = (
"The ratio of residual power in source footprints on the"
63 " difference image to the power in the footprints on the"
64 f
" science image was {ratio}, which exceeds the maximum"
65 f
" threshold of {threshold}")
72 return {
"ratio": self.
ratio,
78 """Raised when there are no diaSources detected on an image difference.
81 msg = (
"No diaSources detected!")
90 """Raised if the cosmic ray task fails with too many cosmics.
95 Maximum number of cosmic rays allowed.
98 msg = f
"Cosmic ray task found more than {maxCosmicRays} cosmics."
102 self.
_metadata[
"maxCosmicRays"] = maxCosmicRays
106 return f
"{self.msg}: {self.metadata}"
110 for key, value
in self.
_metadata.items():
111 if not (isinstance(value, int)
or isinstance(value, float)
or isinstance(value, str)):
112 raise TypeError(f
"{key} is of type {type(value)}, but only (int, float, str) are allowed.")
117 dimensions=(
"instrument",
"visit",
"detector"),
118 defaultTemplates={
"coaddName":
"deep",
119 "warpTypeSuffix":
"",
121 science = pipeBase.connectionTypes.Input(
122 doc=
"Input science exposure.",
123 dimensions=(
"instrument",
"visit",
"detector"),
124 storageClass=
"ExposureF",
125 name=
"{fakesType}calexp"
127 matchedTemplate = pipeBase.connectionTypes.Input(
128 doc=
"Warped and PSF-matched template used to create the difference image.",
129 dimensions=(
"instrument",
"visit",
"detector"),
130 storageClass=
"ExposureF",
131 name=
"{fakesType}{coaddName}Diff_matchedExp",
133 difference = pipeBase.connectionTypes.Input(
134 doc=
"Result of subtracting template from science.",
135 dimensions=(
"instrument",
"visit",
"detector"),
136 storageClass=
"ExposureF",
137 name=
"{fakesType}{coaddName}Diff_differenceTempExp",
139 kernelSources = pipeBase.connectionTypes.Input(
140 doc=
"Final selection of sources used for psf matching.",
141 dimensions=(
"instrument",
"visit",
"detector"),
142 storageClass=
"SourceCatalog",
143 name=
"{fakesType}{coaddName}Diff_psfMatchSources"
145 outputSchema = pipeBase.connectionTypes.InitOutput(
146 doc=
"Schema (as an example catalog) for output DIASource catalog.",
147 storageClass=
"SourceCatalog",
148 name=
"{fakesType}{coaddName}Diff_diaSrc_schema",
150 diaSources = pipeBase.connectionTypes.Output(
151 doc=
"Detected diaSources on the difference image.",
152 dimensions=(
"instrument",
"visit",
"detector"),
153 storageClass=
"SourceCatalog",
154 name=
"{fakesType}{coaddName}Diff_diaSrc",
156 subtractedMeasuredExposure = pipeBase.connectionTypes.Output(
157 doc=
"Difference image with detection mask plane filled in.",
158 dimensions=(
"instrument",
"visit",
"detector"),
159 storageClass=
"ExposureF",
160 name=
"{fakesType}{coaddName}Diff_differenceExp",
162 differenceBackground = pipeBase.connectionTypes.Output(
163 doc=
"Background model that was subtracted from the difference image.",
164 dimensions=(
"instrument",
"visit",
"detector"),
165 storageClass=
"Background",
166 name=
"difference_background",
168 maskedStreaks = pipeBase.connectionTypes.Output(
169 doc=
'Catalog of streak fit parameters for the difference image.',
170 storageClass=
"ArrowNumpyDict",
171 dimensions=(
"instrument",
"visit",
"detector"),
172 name=
"{fakesType}{coaddName}Diff_streaks",
174 glintTrailInfo = pipeBase.connectionTypes.Output(
175 doc=
'Dict of fit parameters for glint trails in the catalog.',
176 storageClass=
"ArrowNumpyDict",
177 dimensions=(
"instrument",
"visit",
"detector"),
178 name=
"trailed_glints",
181 def __init__(self, *, config):
182 super().__init__(config=config)
183 if not (self.config.doCalculateResidualMetics):
184 self.inputs.remove(
"kernelSources")
185 if not (self.config.writeStreakInfo
and self.config.doMaskStreaks):
186 self.outputs.remove(
"maskedStreaks")
188 if (self.config.image_type ==
"future"
189 or not (self.config.doSubtractBackground
and self.config.doWriteBackground)):
190 self.outputs.remove(
"differenceBackground")
191 if not (self.config.writeGlintInfo):
192 self.outputs.remove(
"glintTrailInfo")
193 if self.config.image_type ==
"future":
194 self.difference = dataclasses.replace(self.difference, storageClass=
"DifferenceImage")
195 self.subtractedMeasuredExposure = dataclasses.replace(self.subtractedMeasuredExposure,
196 storageClass=
"DifferenceImage")
199class DetectAndMeasureConfig(pipeBase.PipelineTaskConfig,
200 pipelineConnections=DetectAndMeasureConnections):
201 """Config for DetectAndMeasureTask
203 doMerge = pexConfig.Field(
206 doc=
"Merge positive and negative diaSources with grow radius "
207 "set by growFootprint"
209 doForcedMeasurement = pexConfig.Field(
212 doc=
"Force photometer diaSource locations on PVI?"
214 doAddMetrics = pexConfig.Field(
217 doc=
"Add columns to the source table to hold analysis metrics?"
219 doSubtractBackground = pexConfig.Field(
221 doc=
"Subtract a background model from the image before detection?",
224 doWriteBackground = pexConfig.Field(
226 doc=
"Persist the fitted background model? Ignored when"
227 " image_type='future', which records it on the output images.",
229 deprecated=
"Use image_type='future', which records the background on the output images."
230 " Will be removed after v31.",
232 doCalculateResidualMetics = pexConfig.Field(
234 doc=
"Calculate metrics to assess image subtraction quality for the task"
238 subtractInitialBackground = pexConfig.ConfigurableField(
239 target=lsst.meas.algorithms.SubtractBackgroundTask,
240 doc=
"Task to perform intial background subtraction, before first detection pass.",
242 subtractFinalBackground = pexConfig.ConfigurableField(
243 target=lsst.meas.algorithms.SubtractBackgroundTask,
244 doc=
"Task to perform final background subtraction, after first detection pass.",
246 doFindCosmicRays = pexConfig.Field(
248 doc=
"Detect and mask cosmic rays on the difference image?"
249 "CRs can be interpolated over by setting cosmicray.keepCRs=False",
252 cosmicray = pexConfig.ConfigField(
253 dtype=FindCosmicRaysConfig,
254 doc=
"Options for finding and masking cosmic rays",
256 detection = pexConfig.ConfigurableField(
257 target=SourceDetectionTask,
258 doc=
"Final source detection for diaSource measurement",
260 streakDetection = pexConfig.ConfigurableField(
261 target=SourceDetectionTask,
262 doc=
"Separate source detection used only for streak masking",
264 doDeblend = pexConfig.Field(
267 doc=
"Deblend DIASources after detection?"
269 deblend = pexConfig.ConfigurableField(
270 target=lsst.meas.deblender.SourceDeblendTask,
271 doc=
"Task to split blended sources into their components."
273 measurement = pexConfig.ConfigurableField(
274 target=DipoleFitTask,
275 doc=
"Task to measure sources on the difference image.",
277 doApCorr = lsst.pex.config.Field(
280 doc=
"Run subtask to apply aperture corrections"
282 applyApCorr = lsst.pex.config.ConfigurableField(
283 target=ApplyApCorrTask,
284 doc=
"Task to apply aperture corrections"
286 forcedMeasurement = pexConfig.ConfigurableField(
287 target=ForcedMeasurementTask,
288 doc=
"Task to force photometer science image at diaSource locations.",
290 growFootprint = pexConfig.Field(
293 doc=
"Grow positive and negative footprints by this many pixels before merging"
295 diaSourceMatchRadius = pexConfig.Field(
298 doc=
"Match radius (in arcseconds) for DiaSource to Source association"
300 doSkySources = pexConfig.Field(
303 doc=
"Generate sky sources?",
305 skySources = pexConfig.ConfigurableField(
306 target=SkyObjectsTask,
307 doc=
"Generate sky sources",
309 doMaskStreaks = pexConfig.Field(
312 doc=
"Turn on streak masking",
314 maskStreaks = pexConfig.ConfigurableField(
315 target=MaskStreaksTask,
316 doc=
"Subtask for masking streaks. Only used if doMaskStreaks is True. "
317 "Adds a mask plane to an exposure, with the mask plane name set by streakMaskName.",
319 streakBinFactor = pexConfig.Field(
322 doc=
"Bin scale factor to use when rerunning detection for masking streaks. "
323 "Only used if doMaskStreaks is True.",
325 writeStreakInfo = pexConfig.Field(
328 doc=
"Record the parameters of any detected streaks. For LSST, this should be turned off except for "
331 findGlints = pexConfig.ConfigurableField(
332 target=FindGlintTrailsTask,
333 doc=
"Subtask for finding glint trails, usually caused by satellites or debris."
335 writeGlintInfo = pexConfig.Field(
338 doc=
"Record the parameters of any detected glint trails."
340 setPrimaryFlags = pexConfig.ConfigurableField(
341 target=SetPrimaryFlagsTask,
342 doc=
"Task to add isPrimary and deblending-related flags to the catalog."
344 badSourceFlags = lsst.pex.config.ListField(
346 doc=
"Sources with any of these flags set are removed before writing the output catalog.",
347 default=(
"base_PixelFlags_flag_offimage",
348 "base_PixelFlags_flag_interpolatedCenterAll",
349 "base_PixelFlags_flag_badCenter",
350 "base_PixelFlags_flag_edgeCenter",
351 "base_PixelFlags_flag_nodataCenter",
352 "base_PixelFlags_flag_saturatedCenter",
353 "base_PixelFlags_flag_sat_templateCenter",
354 "base_PixelFlags_flag_spikeCenter",
357 clearMaskPlanes = lsst.pex.config.ListField(
359 doc=
"Mask planes to clear before running detection.",
360 default=(
"DETECTED",
"DETECTED_NEGATIVE",
"NOT_DEBLENDED",
"STREAK"),
362 doRejectBadMaskPlaneDetections = pexConfig.Field(
365 doc=
"Reject any peaks detected on ``badMaskPlanes`` before measurement."
366 "These should all be rejected downstream by ``badSourceFlags``, "
367 "but filtering earlier can save time."
369 badMaskPlanes = lsst.pex.config.ListField(
371 doc=
"Detections whose footprint peak lies on a pixel with any of these"
372 " mask planes set will be rejected before measurement."
373 " Any missing mask planes will be silently ignored.",
374 default=(
"NO_DATA",
"BAD",
"SAT",
"EDGE"),
376 raiseOnBadSubtractionRatio = pexConfig.Field(
379 doc=
"Raise an error if the ratio of power in detected footprints"
380 " on the difference image to the power in footprints on the science"
381 " image exceeds ``badSubtractionRatioThreshold``",
383 badSubtractionRatioThreshold = pexConfig.Field(
386 doc=
"Maximum ratio of power in footprints on the difference image to"
387 " the same footprints on the science image."
388 "Only used if ``raiseOnBadSubtractionRatio`` is set",
390 badSubtractionVariationThreshold = pexConfig.Field(
393 doc=
"Maximum standard deviation of the ratio of power in footprints on"
394 " the difference image to the same footprints on the science image."
395 "Only used if ``raiseOnBadSubtractionRatio`` is set",
397 raiseOnNoDiaSources = pexConfig.Field(
400 doc=
"Raise an algorithm error if no diaSources are detected.",
402 run_sattle = pexConfig.Field(
405 doc=
"If true, dia source bounding boxes will be sent for verification"
406 "to the sattle service."
408 sattle_historical = pexConfig.Field(
411 doc=
"If re-running a pipeline that requires sattle, this should be set "
412 "to True. This will populate sattle's cache with the historic data "
413 "closest in time to the exposure."
415 idGenerator = DetectorVisitIdGeneratorConfig.make_field()
416 image_type = pexConfig.ChoiceField[str](
417 "Image type of the input and measured difference images (and, for"
418 " DetectAndMeasureScoreTask, the input and measured score images).",
420 "legacy":
"Read and write lsst.afw.image.ExposureF.",
421 "future":
"Read and write lsst.images.DifferenceImage.",
432 self.subtractInitialBackground.binSize = 8
433 self.subtractInitialBackground.useApprox =
False
434 self.subtractInitialBackground.statisticsProperty =
"MEDIAN"
435 self.subtractInitialBackground.doFilterSuperPixels =
True
436 self.subtractInitialBackground.ignoredPixelMask = [
"BAD",
444 self.subtractFinalBackground.binSize = 40
445 self.subtractFinalBackground.useApprox =
False
446 self.subtractFinalBackground.statisticsProperty =
"MEDIAN"
447 self.subtractFinalBackground.doFilterSuperPixels =
True
448 self.subtractFinalBackground.ignoredPixelMask = [
"BAD",
455 self.cosmicray.keepCRs =
True
457 self.detection.thresholdPolarity =
"both"
458 self.detection.thresholdValue = 5.0
459 self.detection.reEstimateBackground =
False
460 self.detection.thresholdType =
"pixel_stdev"
461 self.detection.excludeMaskPlanes = []
464 self.streakDetection.thresholdType = self.detection.thresholdType
465 self.streakDetection.reEstimateBackground =
False
466 self.streakDetection.excludeMaskPlanes = self.detection.excludeMaskPlanes
467 self.streakDetection.thresholdValue = self.detection.thresholdValue
469 self.streakDetection.thresholdPolarity =
"positive"
471 self.streakDetection.nSigmaToGrow = 0
474 self.maskStreaks.onlyMaskDetected =
False
476 self.maskStreaks.maxStreakWidth = 100
479 self.maskStreaks.maxFitIter = 10
481 self.maskStreaks.nSigmaMask = 2
484 self.maskStreaks.absMinimumKernelHeight = 2
486 self.measurement.plugins.names |= [
"ext_trailedSources_Naive",
487 "base_LocalPhotoCalib",
489 "ext_shapeHSM_HsmSourceMoments",
490 "ext_shapeHSM_HsmPsfMoments",
491 "base_ClassificationSizeExtendedness",
493 self.measurement.slots.psfShape =
"ext_shapeHSM_HsmPsfMoments"
494 self.measurement.slots.shape =
"ext_shapeHSM_HsmSourceMoments"
495 self.measurement.plugins[
"base_SdssCentroid"].maxDistToPeak = 5.0
496 self.forcedMeasurement.plugins = [
"base_TransformedCentroid",
"base_PsfFlux"]
497 self.forcedMeasurement.copyColumns = {
498 "id":
"objectId",
"parent":
"parentObjectId",
"coord_ra":
"coord_ra",
"coord_dec":
"coord_dec"}
499 self.forcedMeasurement.slots.centroid =
"base_TransformedCentroid"
500 self.forcedMeasurement.slots.shape =
None
503 self.measurement.plugins[
"base_PixelFlags"].masksFpAnywhere = [
504 "STREAK",
"INJECTED",
"INJECTED_TEMPLATE",
"HIGH_VARIANCE",
"SAT_TEMPLATE",
"SPIKE"]
505 self.measurement.plugins[
"base_PixelFlags"].masksFpCenter = [
506 "STREAK",
"INJECTED",
"INJECTED_TEMPLATE",
"HIGH_VARIANCE",
"SAT_TEMPLATE",
"SPIKE"]
507 self.skySources.avoidMask = [
"DETECTED",
"DETECTED_NEGATIVE",
"BAD",
"NO_DATA",
"EDGE"]
513 if not os.getenv(
"SATTLE_URI_BASE"):
514 raise pexConfig.FieldValidationError(DetectAndMeasureConfig.run_sattle, self,
515 "Sattle requested but SATTLE_URI_BASE "
516 "environment variable not set.")
519class DetectAndMeasureTask(lsst.pipe.base.PipelineTask):
520 """Detect and measure sources on a difference image.
522 ConfigClass = DetectAndMeasureConfig
523 _DefaultName =
"detectAndMeasure"
525 def __init__(self, **kwargs):
526 super().__init__(**kwargs)
527 self.schema = afwTable.SourceTable.makeMinimalSchema()
530 if self.config.doSubtractBackground:
531 self.makeSubtask(
"subtractInitialBackground")
532 self.makeSubtask(
"subtractFinalBackground")
533 self.makeSubtask(
"detection", schema=self.schema)
534 if self.config.doDeblend:
535 self.makeSubtask(
"deblend", schema=self.schema)
536 self.makeSubtask(
"setPrimaryFlags", schema=self.schema, isSingleFrame=
True)
537 self.makeSubtask(
"measurement", schema=self.schema,
538 algMetadata=self.algMetadata)
539 if self.config.doApCorr:
540 self.makeSubtask(
"applyApCorr", schema=self.measurement.schema)
541 if self.config.doForcedMeasurement:
542 self.schema.addField(
543 "ip_diffim_forced_PsfFlux_instFlux",
"D",
544 "Forced PSF flux measured on the direct image.",
546 self.schema.addField(
547 "ip_diffim_forced_PsfFlux_instFluxErr",
"D",
548 "Forced PSF flux error measured on the direct image.",
550 self.schema.addField(
551 "ip_diffim_forced_PsfFlux_area",
"F",
552 "Forced PSF flux effective area of PSF.",
554 self.schema.addField(
555 "ip_diffim_forced_PsfFlux_flag",
"Flag",
556 "Forced PSF flux general failure flag.")
557 self.schema.addField(
558 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
"Flag",
559 "Forced PSF flux not enough non-rejected pixels in data to attempt the fit.")
560 self.schema.addField(
561 "ip_diffim_forced_PsfFlux_flag_edge",
"Flag",
562 "Forced PSF flux object was too close to the edge of the image to use the full PSF model.")
563 self.schema.addField(
564 "ip_diffim_forced_template_PsfFlux_instFlux",
"D",
565 "Forced PSF flux measured on the template image.",
567 self.schema.addField(
568 "ip_diffim_forced_template_PsfFlux_instFluxErr",
"D",
569 "Forced PSF flux error measured on the template image.",
571 self.schema.addField(
572 "ip_diffim_forced_template_PsfFlux_area",
"F",
573 "Forced template PSF flux effective area of PSF.",
575 self.schema.addField(
576 "ip_diffim_forced_template_PsfFlux_flag",
"Flag",
577 "Forced template PSF flux general failure flag.")
578 self.schema.addField(
579 "ip_diffim_forced_template_PsfFlux_flag_noGoodPixels",
"Flag",
580 "Forced template PSF flux not enough non-rejected pixels in data to attempt the fit.")
581 self.schema.addField(
582 "ip_diffim_forced_template_PsfFlux_flag_edge",
"Flag",
583 """Forced template PSF flux object was too close to the edge of the image """
584 """to use the full PSF model.""")
585 self.makeSubtask(
"forcedMeasurement", refSchema=self.schema)
587 self.schema.addField(
"refMatchId",
"L",
"unique id of reference catalog match")
588 self.schema.addField(
"srcMatchId",
"L",
"unique id of source match")
591 self.makeSubtask(
"skySources", schema=self.schema)
592 if self.config.doMaskStreaks:
593 self.makeSubtask(
"maskStreaks")
594 self.makeSubtask(
"streakDetection")
595 self.makeSubtask(
"findGlints")
596 self.schema.addField(
"glint_trail",
"Flag",
"DiaSource is part of a glint trail.")
597 self.schema.addField(
"reliability", type=
"F", doc=
"Reliability score of the DiaSource")
598 self.schema.addField(
"reliabilityVersion", type=str, size=7, doc=
"Version of the reliability model")
605 for flag
in self.config.badSourceFlags:
606 if flag
not in self.schema:
607 raise pipeBase.InvalidQuantumError(
"Field %s not in schema" % flag)
610 self.outputSchema = afwTable.SourceCatalog(self.schema)
611 self.outputSchema.getTable().setMetadata(self.algMetadata)
613 def runQuantum(self, butlerQC: pipeBase.QuantumContext,
614 inputRefs: pipeBase.InputQuantizedConnection,
615 outputRefs: pipeBase.OutputQuantizedConnection):
616 inputs = butlerQC.get(inputRefs)
617 idGenerator = self.config.idGenerator.apply(butlerQC.quantum.dataId)
618 idFactory = idGenerator.make_table_id_factory()
622 if self.config.image_type ==
"future":
623 dataId = butlerQC.quantum.dataId
624 for output, connection
in ((
"subtractedMeasuredExposure",
"difference"),
625 (
"scoreMeasuredExposure",
"scoreExposure")):
626 if connection
not in inputs:
628 image = inputs[connection]
631 get_difference_image_provenance(image,
"kernel", connection, dataId)
632 get_difference_image_provenance(image,
"templates", connection, dataId)
633 inputImages[output] = image
634 inputs[connection] = image.to_legacy()
638 measurementResults = pipeBase.Struct(
639 subtractedMeasuredExposure=
None,
640 scoreMeasuredExposure=
None,
643 differenceBackground=
None,
646 self.run(**inputs, idFactory=idFactory, measurementResults=measurementResults)
647 except pipeBase.AlgorithmError
as e:
649 error = pipeBase.AnnotatedPartialOutputsError.annotate(
652 measurementResults.subtractedMeasuredExposure,
653 measurementResults.scoreMeasuredExposure,
654 measurementResults.diaSources,
655 measurementResults.maskedStreaks,
658 if self.config.image_type ==
"future":
659 self.convert_outputs_to_future(measurementResults, inputImages)
660 butlerQC.put(measurementResults, outputRefs)
662 if self.config.image_type ==
"future":
663 self.convert_outputs_to_future(measurementResults, inputImages)
664 butlerQC.put(measurementResults, outputRefs)
666 def convert_outputs_to_future(self, results, inputImages):
667 """Write the output images in a result struct back into the
668 `lsst.images` images they were measured from.
670 The image, variance, and mask planes of each input image are replaced
671 by those of the corresponding legacy output, and the background
672 subtracted by this task is attached. The kernel, template records,
673 and observation metadata of the input are kept. Outputs that are
674 absent or `None` are skipped.
678 results : `lsst.pipe.base.Struct`
679 Output struct to read and modify in place.
680 inputImages : `dict` [`str`, `lsst.images.DifferenceImage`]
681 Input image each output was measured from, keyed by the name of
682 the output. Each is modified in place.
686 This uses the default legacy mask plane map of
687 `lsst.images.get_legacy_difference_image_mask_planes` and adds any
688 planes from `~lsst.images.get_legacy_optional_mask_planes` that are set
689 for any pixels in the image.
691 background = getattr(results,
"differenceBackground",
None)
692 for name, image
in inputImages.items():
693 exposure = getattr(results, name,
None)
698 image.image.array[...] = exposure.image.array
699 image.variance.array[...] = exposure.variance.array
700 image.mask = Mask.from_legacy(exposure.mask, get_legacy_difference_image_mask_planes(),
701 sky_projection=image.sky_projection)
702 if background
is not None and len(background) > 0:
705 image.backgrounds.add(
707 field_from_legacy_background(background, bounds=image.bbox, unit=image.unit),
708 description=
"Background subtracted from this image before detection.",
711 setattr(results, name, image)
712 if background
is not None and len(background) == 0:
715 self.log.debug(
"No background model to attach to the output images.")
718 def run(self, science, matchedTemplate, difference, kernelSources=None,
719 idFactory=None, measurementResults=None):
720 """Detect and measure sources on a difference image.
722 The difference image will be convolved with a gaussian approximation of
723 the PSF to form a maximum likelihood image for detection.
724 Close positive and negative detections will optionally be merged into
726 Sky sources, or forced detections in background regions, will optionally
727 be added, and the configured measurement algorithm will be run on all
732 science : `lsst.afw.image.ExposureF`
733 Science exposure that the template was subtracted from.
734 matchedTemplate : `lsst.afw.image.ExposureF`
735 Warped and PSF-matched template that was used produce the
737 difference : `lsst.afw.image.ExposureF`
738 Result of subtracting template from the science image.
739 kernelSources : `lsst.afw.table.SourceCatalog`, optional
740 Final selection of sources that was used for psf matching.
741 idFactory : `lsst.afw.table.IdFactory`, optional
742 Generator object used to assign ids to detected sources in the
743 difference image. Ids from this generator are not set until after
744 deblending and merging positive/negative peaks.
745 measurementResults : `lsst.pipe.base.Struct`, optional
746 Result struct that is modified to allow saving of partial outputs
747 for some failure conditions. If the task completes successfully,
748 this is also returned.
752 measurementResults : `lsst.pipe.base.Struct`
754 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
755 Subtracted exposure with detection mask applied.
756 ``diaSources`` : `lsst.afw.table.SourceCatalog`
757 The catalog of detected sources.
758 ``differenceBackground`` : `lsst.afw.math.BackgroundList`
759 Background that was subtracted from the difference image.
761 if measurementResults
is None:
762 measurementResults = pipeBase.Struct()
763 if idFactory
is None:
764 idFactory = lsst.meas.base.IdGenerator().make_table_id_factory()
767 self._prepareInputs(difference)
768 if self.config.doSubtractBackground:
769 background = self._fitAndSubtractBackground(differenceExposure=difference)
771 background = afwMath.BackgroundList()
773 if self.config.doFindCosmicRays:
774 self.findAndMaskCosmicRays(difference)
779 table = afwTable.SourceTable.make(self.schema)
780 results = self.detection.run(
784 background=background,
787 measurementResults.differenceBackground = background
789 if self.config.doDeblend:
790 sources, positives, negatives = self._deblend(difference,
795 positives = afwTable.SourceCatalog(self.schema)
796 results.positive.makeSources(positives)
797 negatives = afwTable.SourceCatalog(self.schema)
798 results.negative.makeSources(negatives)
799 sources = results.sources
801 self.processResults(science, matchedTemplate, difference,
802 sources, idFactory, kernelSources,
805 measurementResults=measurementResults)
806 return measurementResults
808 def _prepareInputs(self, difference):
809 """Ensure that we start with an empty detection and deblended mask.
813 difference : `lsst.afw.image.ExposureF`
814 The difference image that will be used for detecting diaSources.
815 The mask plane will be modified in place.
819 lsst.pipe.base.UpstreamFailureNoWorkFound
820 If the PSF is not usable for measurement.
823 sigma = difference.psf.computeShape(difference.psf.getAveragePosition()).getDeterminantRadius()
825 raise pipeBase.UpstreamFailureNoWorkFound(
"Invalid PSF detected! PSF width evaluates to NaN.")
827 mask = difference.mask
828 for mp
in self.config.clearMaskPlanes:
829 if mp
not in mask.getMaskPlaneDict():
830 mask.addMaskPlane(mp)
831 mask &= ~mask.getPlaneBitMask(self.config.clearMaskPlanes)
833 def _fitAndSubtractBackground(self, differenceExposure, scoreExposure=None):
834 """Fit the final background to ``differenceExposure``.
836 A rough background is first fit on the difference and an internal
837 first-pass detection is run on the (background-subtracted) clone; the
838 resulting detection mask is then transferred to ``differenceExposure``
839 so the final background fit can mask out real sources. This two-pass
840 scheme is what suppresses spurious detections from glints and
841 extended structures: the small-binsize initial fit aggressively
842 over-subtracts those features, and the resulting peak mask lets the
843 large-binsize final fit produce a clean background. When
844 ``scoreExposure`` is supplied (score-image variant), the final
845 background is also subtracted from its image so the caller's final
846 detection runs on a properly background-subtracted score.
848 The first-pass detection's `Struct` is discarded; only the mask it
849 leaves on the clone is used downstream.
853 differenceExposure : `lsst.afw.image.ExposureF`
854 Difference image. The final background is subtracted from its
855 image in place but its mask is not modified.
856 scoreExposure : `lsst.afw.image.ExposureF`, optional
857 Maximum-likelihood score image. When supplied, the final
858 background is also subtracted from its image.
862 background : `lsst.afw.math.BackgroundList`
863 The fit final background.
865 if scoreExposure
is None:
866 detectionExposure = differenceExposure.clone()
867 background = self.subtractInitialBackground.run(detectionExposure).background
872 background = self.subtractInitialBackground.run(differenceExposure.clone()).background
873 detectionExposure = scoreExposure.clone()
874 detectionExposure.image -= background.getImage()
876 table = afwTable.SourceTable.make(self.schema)
879 exposure=detectionExposure,
881 background=background,
887 mask = differenceExposure.mask
888 originalMask = mask.array.copy()
889 detectedBit = mask.getPlaneBitMask([
"DETECTED"])
890 detectedPix = detectionExposure.mask.array & detectedBit > 0
891 detectedNegativeBit = mask.getPlaneBitMask([
"DETECTED_NEGATIVE"])
892 detectedNegativePix = detectionExposure.mask.array & detectedNegativeBit > 0
893 mask.array[detectedPix] |= detectedBit
894 mask.array[detectedNegativePix] |= detectedNegativeBit
895 background = self.subtractFinalBackground.run(differenceExposure).background
896 mask.array[...] = originalMask
897 if scoreExposure
is not None:
900 scoreExposure.image -= background.getImage()
903 def processResults(self, science, matchedTemplate, difference, sources, idFactory,
904 kernelSources=None, positives=None, negatives=None, measurementResults=None):
905 """Measure and process the results of source detection.
909 science : `lsst.afw.image.ExposureF`
910 Science exposure that the template was subtracted from.
911 matchedTemplate : `lsst.afw.image.ExposureF`
912 Warped and PSF-matched template that was used produce the
914 difference : `lsst.afw.image.ExposureF`
915 Result of subtracting template from the science image.
916 sources : `lsst.afw.table.SourceCatalog`
917 Detected sources on the difference exposure.
918 idFactory : `lsst.afw.table.IdFactory`
919 Generator object used to assign ids to detected sources in the
921 kernelSources : `lsst.afw.table.SourceCatalog`, optional
922 Final selection of sources that was used for psf matching.
923 positives : `lsst.afw.table.SourceCatalog`, optional
924 Positive polarity footprints.
925 negatives : `lsst.afw.table.SourceCatalog`, optional
926 Negative polarity footprints.
927 measurementResults : `lsst.pipe.base.Struct`, optional
928 Result struct that is modified to allow saving of partial outputs
929 for some failure conditions. If the task completes successfully,
930 this is also returned.
934 measurementResults : `lsst.pipe.base.Struct`
936 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
937 Subtracted exposure with detection mask applied.
938 ``diaSources`` : `lsst.afw.table.SourceCatalog`
939 The catalog of detected sources.
941 if measurementResults
is None:
942 measurementResults = pipeBase.Struct()
943 self.metadata[
"nUnmergedDiaSources"] = len(sources)
944 if self.config.doMerge:
946 if len(positives) > 0:
947 peakSchema = positives[0].getFootprint().peaks.schema
948 elif len(negatives) > 0:
949 peakSchema = negatives[0].getFootprint().peaks.schema
951 peakSchema = afwDetection.PeakTable.makeMinimalSchema()
952 mergeList = afwDetection.FootprintMergeList(self.schema,
953 [
"positive",
"negative"], peakSchema)
954 initialDiaSources = afwTable.SourceCatalog(self.schema)
958 mergeList.addCatalog(initialDiaSources.table, positives,
"positive", minNewPeakDist=0)
959 mergeList.addCatalog(initialDiaSources.table, negatives,
"negative", minNewPeakDist=0)
960 mergeList.getFinalSources(initialDiaSources)
963 initialDiaSources[
"is_negative"] = initialDiaSources[
"merge_footprint_negative"] & \
964 ~initialDiaSources[
"merge_footprint_positive"]
965 self.log.info(
"Merging detections into %d sources", len(initialDiaSources))
968 self._reorderPeaksBySignificance(initialDiaSources)
970 initialDiaSources = sources
974 for source
in initialDiaSources:
977 initialDiaSources.getTable().setIdFactory(idFactory)
978 initialDiaSources.setMetadata(self.algMetadata)
980 self.metadata[
"nMergedDiaSources"] = len(initialDiaSources)
982 if self.config.doMaskStreaks:
983 streakInfo = self._runStreakMasking(difference)
985 if self.config.doSkySources:
986 self.addSkySources(initialDiaSources, difference.mask, difference.info.id)
988 if self.config.doRejectBadMaskPlaneDetections:
991 initialDiaSources = self._rejectBadMaskedDetections(initialDiaSources, difference.mask)
993 if not initialDiaSources.isContiguous():
994 initialDiaSources = initialDiaSources.copy(deep=
True)
996 self.measureDiaSources(initialDiaSources, science, difference, matchedTemplate)
999 diaSources = self._removeBadSources(initialDiaSources)
1001 if self.config.run_sattle:
1002 diaSources = self.filterSatellites(diaSources, science)
1005 diaSources, trail_parameters = self._find_glint_trails(diaSources)
1006 if self.config.writeGlintInfo:
1007 measurementResults.mergeItems(trail_parameters,
'glintTrailInfo')
1009 if self.config.doForcedMeasurement:
1010 self.measureForcedSources(diaSources, science, difference.getWcs())
1011 self.measureForcedSources(diaSources, matchedTemplate, difference.getWcs(),
1018 difference.image.array[difference.mask.array & difference.mask.getPlaneBitMask(
'NO_DATA') > 0] = 0
1020 measurementResults.subtractedMeasuredExposure = difference
1022 if self.config.doMaskStreaks
and self.config.writeStreakInfo:
1023 measurementResults.maskedStreaks = streakInfo.maskedStreaks
1025 if kernelSources
is not None:
1026 self.calculateMetrics(science, difference, diaSources, kernelSources)
1028 if np.count_nonzero(~diaSources[
"sky_source"]) > 0:
1029 measurementResults.diaSources = diaSources
1030 elif self.config.raiseOnNoDiaSources:
1032 elif len(diaSources) > 0:
1035 measurementResults.diaSources = diaSources
1036 self.log.info(
"Measured %d diaSources and %d sky sources",
1037 np.count_nonzero(~diaSources[
"sky_source"]),
1038 np.count_nonzero(diaSources[
"sky_source"])
1040 return measurementResults
1042 def _reorderPeaksBySignificance(self, diaSources):
1043 """Sort each merged source's footprint peaks by significance.
1047 diaSources : `lsst.afw.table.SourceCatalog`
1048 Merged sources whose footprint peaks are re-ordered in place.
1050 for source
in diaSources:
1051 peaks = source.getFootprint().getPeaks()
1052 if len(peaks) < 2
or "significance" not in peaks.schema.getNames():
1054 ordered = sorted(peaks, key=
lambda peak: peak[
"significance"], reverse=
True)
1055 newPeaks = afwDetection.PeakCatalog(peaks.table)
1056 newPeaks.extend(ordered)
1058 source.getFootprint().setPeakCatalog(newPeaks)
1060 def _deblend(self, difference, positiveFootprints, negativeFootprints):
1061 """Deblend the positive and negative footprints and return a catalog
1062 containing just the children, and the deblended footprints.
1066 difference : `lsst.afw.image.Exposure`
1067 Result of subtracting template from the science image.
1068 positiveFootprints, negativeFootprints : `lsst.afw.detection.FootprintSet`
1069 Positive and negative polarity footprints measured on
1070 ``difference`` to be deblended separately.
1074 sources : `lsst.afw.table.SourceCatalog`
1075 Positive and negative deblended children.
1076 positives, negatives : `lsst.afw.table.SourceCatalog`
1077 Deblended positive and negative polarity sources with footprints
1078 detected on ``difference``.
1081 def deblend(footprints, negative=False):
1082 """Deblend a positive or negative footprint set,
1083 and return the deblended children.
1087 footprints : `lsst.afw.detection.FootprintSet`
1089 Set True if the footprints contain negative fluxes
1093 sources : `lsst.afw.table.SourceCatalog`
1095 sources = afwTable.SourceCatalog(self.schema)
1096 footprints.makeSources(sources)
1099 difference_inverted = difference.clone()
1100 difference_inverted.image *= -1
1101 self.deblend.run(exposure=difference_inverted, sources=sources)
1102 children = sources[sources[
"parent"] != 0]
1104 for child
in children:
1105 footprint = child.getFootprint()
1106 array = footprint.getImageArray()
1109 self.deblend.run(exposure=difference, sources=sources)
1110 self.setPrimaryFlags.run(sources)
1111 children = sources[
"detect_isDeblendedSource"] == 1
1112 sources = sources[children].copy(deep=
True)
1114 sources[
'parent'] = 0
1115 return sources.copy(deep=
True)
1117 positives = deblend(positiveFootprints)
1118 negatives = deblend(negativeFootprints, negative=
True)
1120 sources = afwTable.SourceCatalog(self.schema)
1121 sources.reserve(len(positives) + len(negatives))
1122 sources.extend(positives, deep=
True)
1123 sources.extend(negatives, deep=
True)
1124 if len(negatives) > 0:
1125 sources[-len(negatives):][
"is_negative"] =
True
1126 return sources, positives, negatives
1128 def _rejectBadMaskedDetections(self, initialDiaSources, mask):
1129 """Remove detections whose footprint peak lies on a bad-masked pixel.
1131 Detections are rejected if any peak of the source footprint falls on a
1132 pixel with any of the ``config.badMaskPlanes`` bits set.
1136 initialDiaSources : `lsst.afw.table.SourceCatalog`
1137 The catalog of detected peaks.
1138 mask : `lsst.afw.image.Mask`
1139 Mask of the image used for detection.
1143 initialDiaSources : `lsst.afw.table.SourceCatalog`
1144 The catalog with bad-masked detections removed.
1146 if not self.config.badMaskPlanes
or len(initialDiaSources) == 0:
1147 self.metadata[
"nRejectedBadMaskPlanes"] = 0
1148 return initialDiaSources
1150 presentPlanes = [mp
for mp
in self.config.badMaskPlanes
1151 if mp
in mask.getMaskPlaneDict()]
1152 if not presentPlanes:
1153 self.metadata[
"nRejectedBadMaskPlanes"] = 0
1154 return initialDiaSources
1156 badBitMask = mask.getPlaneBitMask(presentPlanes)
1157 x0, y0 = mask.getXY0()
1159 selector = np.ones(len(initialDiaSources), dtype=bool)
1160 for i, source
in enumerate(initialDiaSources):
1161 peaks = source.getFootprint().getPeaks()
1163 ix = peak.getIx() - x0
1164 iy = peak.getIy() - y0
1165 if mask.array[iy, ix] & badBitMask:
1168 nRejected = np.count_nonzero(~selector)
1169 self.metadata[
"nRejectedBadMaskPlanes"] = nRejected
1171 self.log.info(
"Rejected %d detections on pixels with bad mask "
1172 "planes %s before measurement.",
1173 nRejected, presentPlanes)
1174 return initialDiaSources[selector].copy(deep=
True)
1176 def _removeBadSources(self, diaSources):
1177 """Remove unphysical diaSources from the catalog.
1181 diaSources : `lsst.afw.table.SourceCatalog`
1182 The catalog of detected sources.
1186 diaSources : `lsst.afw.table.SourceCatalog`
1187 The updated catalog of detected sources, with any source that has a
1188 flag in ``config.badSourceFlags`` set removed.
1190 selector = np.ones(len(diaSources), dtype=bool)
1191 for flag
in self.config.badSourceFlags:
1192 flags = diaSources[flag]
1193 nBad = np.count_nonzero(flags)
1195 self.log.debug(
"Found %d unphysical sources with flag %s.", nBad, flag)
1197 nBadTotal = np.count_nonzero(~selector)
1198 self.metadata[
"nRemovedBadFlaggedSources"] = nBadTotal
1199 self.log.info(
"Removed %d unphysical sources.", nBadTotal)
1200 return diaSources[selector].copy(deep=
True)
1202 def _find_glint_trails(self, diaSources):
1203 """Define a new flag column for diaSources that are in a glint trail.
1207 diaSources : `lsst.afw.table.SourceCatalog`
1208 The catalog of detected sources.
1212 diaSources : `lsst.afw.table.SourceCatalog`
1213 The updated catalog of detected sources, with a new bool column
1214 called 'glint_trail' added.
1216 trail_parameters : `dict`
1217 Parameters of all the trails that were found.
1219 if self.config.doSkySources:
1221 candidateDiaSources = diaSources[~diaSources[
"sky_source"]].copy(deep=
True)
1223 candidateDiaSources = diaSources
1224 trailed_glints = self.findGlints.run(candidateDiaSources)
1225 glint_mask = [
True if id
in trailed_glints.trailed_ids
else False for id
in diaSources[
'id']]
1226 if np.any(glint_mask):
1227 diaSources[
'glint_trail'] = np.array(glint_mask)
1229 slopes = np.array([trail.slope
for trail
in trailed_glints.parameters])
1230 intercepts = np.array([trail.intercept
for trail
in trailed_glints.parameters])
1231 stderrs = np.array([trail.stderr
for trail
in trailed_glints.parameters])
1232 lengths = np.array([trail.length
for trail
in trailed_glints.parameters])
1233 angles = np.array([trail.angle
for trail
in trailed_glints.parameters])
1234 center_xs = np.array([trail.center_x
for trail
in trailed_glints.parameters])
1235 center_ys = np.array([trail.center_y
for trail
in trailed_glints.parameters])
1236 parameters = {
'slopes': slopes,
'intercepts': intercepts,
'stderrs': stderrs,
'lengths': lengths,
1237 'angles': angles,
'center_xs': center_xs,
'center_ys': center_ys}
1239 trail_parameters = pipeBase.Struct(glintTrailInfo=parameters)
1241 return diaSources, trail_parameters
1243 def addSkySources(self, diaSources, mask, seed,
1245 """Add sources in empty regions of the difference image
1246 for measuring the background.
1250 diaSources : `lsst.afw.table.SourceCatalog`
1251 The catalog of detected sources.
1252 mask : `lsst.afw.image.Mask`
1253 Mask plane for determining regions where Sky sources can be added.
1255 Seed value to initialize the random number generator.
1258 subtask = self.skySources
1259 if subtask.config.nSources <= 0:
1260 self.metadata[f
"n_{subtask.getName()}"] = 0
1262 skySourceFootprints = subtask.run(mask=mask, seed=seed, catalog=diaSources)
1263 self.metadata[f
"n_{subtask.getName()}"] = len(skySourceFootprints)
1265 def measureDiaSources(self, diaSources, science, difference, matchedTemplate):
1266 """Use (matched) template and science image to constrain dipole fitting.
1270 diaSources : `lsst.afw.table.SourceCatalog`
1271 The catalog of detected sources.
1272 science : `lsst.afw.image.ExposureF`
1273 Science exposure that the template was subtracted from.
1274 difference : `lsst.afw.image.ExposureF`
1275 Result of subtracting template from the science image.
1276 matchedTemplate : `lsst.afw.image.ExposureF`
1277 Warped and PSF-matched template that was used produce the
1281 for mp
in self.config.measurement.plugins[
"base_PixelFlags"].masksFpAnywhere:
1282 difference.mask.addMaskPlane(mp)
1285 self.measurement.run(diaSources, difference, science, matchedTemplate)
1286 if self.config.doApCorr:
1287 apCorrMap = difference.getInfo().getApCorrMap()
1288 if apCorrMap
is None:
1289 self.log.warning(
"Difference image does not have valid aperture correction; skipping.")
1291 self.applyApCorr.run(
1293 apCorrMap=apCorrMap,
1296 def measureForcedSources(self, diaSources, image, wcs, template=False):
1297 """Perform forced measurement of the diaSources on a direct image.
1301 diaSources : `lsst.afw.table.SourceCatalog`
1302 The catalog of detected sources.
1303 image: `lsst.afw.image.ExposureF`
1304 Exposure that the forced measurement is being performed on
1305 wcs : `lsst.afw.geom.SkyWcs`
1306 Coordinate system definition (wcs) for the exposure.
1308 Is the forced measurement being performed on the template?
1312 forcedSources = self.forcedMeasurement.generateMeasCat(image, diaSources, wcs)
1313 self.forcedMeasurement.run(forcedSources, image, diaSources, wcs)
1316 base_key =
'ip_diffim_forced_template_PsfFlux'
1318 base_key =
'ip_diffim_forced_PsfFlux'
1319 mapper = afwTable.SchemaMapper(forcedSources.schema, diaSources.schema)
1320 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFlux")[0],
1321 f
"{base_key}_instFlux",
True)
1322 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFluxErr")[0],
1323 f
"{base_key}_instFluxErr",
True)
1324 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_area")[0],
1325 f
"{base_key}_area",
True)
1326 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag")[0],
1327 f
"{base_key}_flag",
True)
1328 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_noGoodPixels")[0],
1329 f
"{base_key}_flag_noGoodPixels",
True)
1330 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_edge")[0],
1331 f
"{base_key}_flag_edge",
True)
1332 for diaSource, forcedSource
in zip(diaSources, forcedSources):
1333 diaSource.assign(forcedSource, mapper)
1335 def findAndMaskCosmicRays(self, difference):
1336 """Detect and mask cosmic rays on the difference image.
1340 difference : `lsst.afw.image.Exposure`
1341 The background-subtracted difference image.
1342 The mask plane will be modified in place.
1348 crs = findCosmicRays(
1349 difference.maskedImage,
1352 pexConfig.makePropertySet(self.config.cosmicray),
1353 self.config.cosmicray.keepCRs,
1359 mask = difference.maskedImage.mask
1360 crBit = mask.getPlaneBitMask(
"CR")
1361 afwDetection.setMaskFromFootprintList(mask, crs, crBit)
1364 text =
"masked" if self.config.cosmicray.keepCRs
else "interpolated over"
1365 self.log.info(
"Identified and %s %s cosmic rays.", text, num)
1366 self.metadata[
"cosmic_ray_count"] = num
1368 def calculateMetrics(self, science, difference, diaSources, kernelSources):
1369 """Add difference image QA metrics to the Task metadata.
1371 This may be used to produce corresponding metrics (see
1372 lsst.analysis.tools.tasks.diffimTaskDetectorVisitMetricAnalysis).
1376 science : `lsst.afw.image.ExposureF`
1377 Science exposure that was subtracted.
1378 difference : `lsst.afw.image.Exposure`
1379 The target difference image to calculate metrics for.
1380 diaSources : `lsst.afw.table.SourceCatalog`
1381 The catalog of detected sources.
1382 kernelSources : `lsst.afw.table.SourceCatalog`
1383 Final selection of sources that was used for psf matching.
1385 mask = difference.mask
1386 badPix = (mask.array & mask.getPlaneBitMask(self.config.detection.excludeMaskPlanes)) > 0
1387 self.metadata[
"nGoodPixels"] = np.sum(~badPix)
1388 self.metadata[
"nBadPixels"] = np.sum(badPix)
1389 detPosPix = (mask.array & mask.getPlaneBitMask(
"DETECTED")) > 0
1390 detNegPix = (mask.array & mask.getPlaneBitMask(
"DETECTED_NEGATIVE")) > 0
1391 self.metadata[
"nPixelsDetectedPositive"] = np.sum(detPosPix)
1392 self.metadata[
"nPixelsDetectedNegative"] = np.sum(detNegPix)
1395 self.metadata[
"nBadPixelsDetectedPositive"] = np.sum(detPosPix)
1396 self.metadata[
"nBadPixelsDetectedNegative"] = np.sum(detNegPix)
1398 metricsMaskPlanes = list(mask.getMaskPlaneDict().keys())
1399 for maskPlane
in metricsMaskPlanes:
1401 self.metadata[
"%s_mask_fraction"%maskPlane.lower()] = evaluateMaskFraction(mask, maskPlane)
1402 except InvalidParameterError:
1403 self.metadata[
"%s_mask_fraction"%maskPlane.lower()] = -1
1404 self.log.info(
"Unable to calculate metrics for mask plane %s: not in image"%maskPlane)
1406 if self.config.doSkySources:
1407 skySources = diaSources[diaSources[
"sky_source"]]
1410 metrics = computeDifferenceImageMetrics(science, difference, kernelSources, sky_sources=skySources)
1412 self.metadata[
"residualFootprintRatioMean"] = metrics.differenceFootprintRatioMean
1413 self.metadata[
"residualFootprintRatioStdev"] = metrics.differenceFootprintRatioStdev
1414 self.metadata[
"differenceFootprintSkyRatioMean"] = metrics.differenceFootprintSkyRatioMean
1415 self.metadata[
"differenceFootprintSkyRatioStdev"] = metrics.differenceFootprintSkyRatioStdev
1416 self.log.info(
"Mean, stdev of ratio of difference to science "
1417 "pixels in star footprints: %5.4f, %5.4f",
1418 self.metadata[
"residualFootprintRatioMean"],
1419 self.metadata[
"residualFootprintRatioStdev"])
1420 if self.config.raiseOnBadSubtractionRatio:
1421 if metrics.differenceFootprintRatioMean > self.config.badSubtractionRatioThreshold:
1423 threshold=self.config.badSubtractionRatioThreshold)
1424 if metrics.differenceFootprintRatioStdev > self.config.badSubtractionVariationThreshold:
1426 threshold=self.config.badSubtractionVariationThreshold)
1428 def getSattleDiaSourceAllowlist(self, diaSources, science):
1429 """Query the sattle service and determine which diaSources are allowed.
1433 diaSources : `lsst.afw.table.SourceCatalog`
1434 The catalog of detected sources.
1435 science : `lsst.afw.image.ExposureF`
1436 Science exposure that was subtracted.
1440 allow_list : `list` of `int`
1441 diaSourceIds of diaSources that can be made public.
1446 Raised if sattle call does not return success.
1448 wcs = science.getWcs()
1449 visit_info = science.getInfo().getVisitInfo()
1450 visit_id = visit_info.getId()
1451 sattle_uri_base = os.getenv(
'SATTLE_URI_BASE')
1453 dia_sources_json = []
1454 for source
in diaSources:
1455 source_bbox = source.getFootprint().getBBox()
1456 corners = wcs.pixelToSky([
lsst.geom.Point2D(c)
for c
in source_bbox.getCorners()])
1457 bbox_radec = [[pt.getRa().asDegrees(), pt.getDec().asDegrees()]
for pt
in corners]
1458 dia_sources_json.append({
"diasource_id": source[
"id"],
"bbox": bbox_radec})
1460 payload = {
"visit_id": visit_id,
"detector_id": science.getDetector().getId(),
1461 "diasources": dia_sources_json,
"historical": self.config.sattle_historical}
1463 sattle_output = requests.put(f
'{sattle_uri_base}/diasource_allow_list',
1467 if sattle_output.status_code == 404:
1468 self.log.warning(f
'Visit {visit_id} not found in sattle cache, re-sending')
1469 populate_sattle_visit_cache(visit_info, historical=self.config.sattle_historical)
1470 sattle_output = requests.put(f
'{sattle_uri_base}/diasource_allow_list', json=payload)
1472 sattle_output.raise_for_status()
1474 return sattle_output.json()[
'allow_list']
1477 def filterSatellites(self, diaSources, science):
1478 """Remove diaSources overlapping predicted satellite positions.
1482 diaSources : `lsst.afw.table.SourceCatalog`
1483 The catalog of detected sources.
1484 science : `lsst.afw.image.ExposureF`
1485 Science exposure that was subtracted.
1489 filterdDiaSources : `lsst.afw.table.SourceCatalog`
1490 Filtered catalog of diaSources
1493 allow_list = self.getSattleDiaSourceAllowlist(diaSources, science)
1496 allow_set = set(allow_list)
1497 allowed_ids = [source[
'id']
in allow_set
for source
in diaSources]
1498 diaSources = diaSources[np.array(allowed_ids)].copy(deep=
True)
1500 self.log.warning(
'Sattle allowlist is empty, all diaSources removed')
1501 diaSources = diaSources[0:0].copy(deep=
True)
1504 def _runStreakMasking(self, difference):
1505 """Do streak masking and optionally save the resulting streak
1506 fit parameters in a catalog.
1508 Only returns non-empty streakInfo if self.config.writeStreakInfo
1509 is set. The difference image is binned by self.config.streakBinFactor
1510 (and detection is run a second time) so that regions with lower
1511 surface brightness streaks are more likely to fall above the
1512 detection threshold.
1516 difference: `lsst.afw.image.Exposure`
1517 The exposure in which to search for streaks. Must have a detection
1522 streakInfo: `lsst.pipe.base.Struct`
1523 ``rho`` : `np.ndarray`
1524 Angle of detected streak.
1525 ``theta`` : `np.ndarray`
1526 Distance from center of detected streak.
1527 ``sigma`` : `np.ndarray`
1528 Width of streak profile.
1529 ``reducedChi2`` : `np.ndarray`
1530 Reduced chi2 of the best-fit streak profile.
1531 ``modelMaximum`` : `np.ndarray`
1532 Peak value of the fit line profile.
1534 maskedImage = difference.maskedImage
1537 self.config.streakBinFactor,
1538 self.config.streakBinFactor)
1539 binnedExposure = afwImage.ExposureF(binnedMaskedImage.getBBox())
1540 binnedExposure.setMaskedImage(binnedMaskedImage)
1542 binnedExposure.mask &= ~binnedExposure.mask.getPlaneBitMask(
'DETECTED')
1544 sigma = difference.psf.computeShape(difference.psf.getAveragePosition()).getDeterminantRadius()
1545 _table = afwTable.SourceTable.make(afwTable.SourceTable.makeMinimalSchema())
1546 self.streakDetection.run(table=_table, exposure=binnedExposure, doSmooth=
True,
1547 sigma=sigma/self.config.streakBinFactor)
1548 binnedDetectedMaskPlane = binnedExposure.mask.array & binnedExposure.mask.getPlaneBitMask(
'DETECTED')
1549 rescaledDetectedMaskPlane = binnedDetectedMaskPlane.repeat(self.config.streakBinFactor,
1550 axis=0).repeat(self.config.streakBinFactor,
1553 streakMaskedImage = maskedImage.clone()
1554 ysize, xsize = rescaledDetectedMaskPlane.shape
1555 streakMaskedImage.mask.array[:ysize, :xsize] |= rescaledDetectedMaskPlane
1557 streaks = self.maskStreaks.run(streakMaskedImage)
1558 streakMaskPlane = streakMaskedImage.mask.array & streakMaskedImage.mask.getPlaneBitMask(
'STREAK')
1560 maskedImage.mask.array |= streakMaskPlane
1562 if self.config.writeStreakInfo:
1563 rhos = np.array([line.rho
for line
in streaks.lines])
1564 thetas = np.array([line.theta
for line
in streaks.lines])
1565 sigmas = np.array([line.sigma
for line
in streaks.lines])
1566 chi2s = np.array([line.reducedChi2
for line
in streaks.lines])
1567 modelMaximums = np.array([line.modelMaximum
for line
in streaks.lines])
1568 streakInfo = {
'rho': rhos,
'theta': thetas,
'sigma': sigmas,
'reducedChi2': chi2s,
1569 'modelMaximum': modelMaximums}
1571 streakInfo = {
'rho': np.array([]),
'theta': np.array([]),
'sigma': np.array([]),
1572 'reducedChi2': np.array([]),
'modelMaximum': np.array([])}
1573 return pipeBase.Struct(maskedStreaks=streakInfo)
1577 scoreExposure = pipeBase.connectionTypes.Input(
1578 doc=
"Maximum likelihood image for detection.",
1579 dimensions=(
"instrument",
"visit",
"detector"),
1580 storageClass=
"ExposureF",
1581 name=
"{fakesType}{coaddName}Diff_scoreTempExp",
1583 scoreMeasuredExposure = pipeBase.connectionTypes.Output(
1584 doc=
"Score image after backgrond subtraction and detection.",
1585 dimensions=(
"instrument",
"visit",
"detector"),
1586 storageClass=
"ExposureF",
1587 name=
"{fakesType}{coaddName}Diff_scoreExp",
1590 def __init__(self, *, config):
1591 super().__init__(config=config)
1592 if self.config.image_type ==
"future":
1593 self.scoreExposure = dataclasses.replace(self.scoreExposure, storageClass=
"DifferenceImage")
1594 self.scoreMeasuredExposure = dataclasses.replace(self.scoreMeasuredExposure,
1595 storageClass=
"DifferenceImage")
1598class DetectAndMeasureScoreConfig(DetectAndMeasureConfig,
1599 pipelineConnections=DetectAndMeasureScoreConnections):
1603class DetectAndMeasureScoreTask(DetectAndMeasureTask):
1604 """Detect DIA sources using a score image,
1605 and measure the detections on the difference image.
1607 Source detection is run on the supplied score, or maximum likelihood,
1608 image. Note that no additional convolution will be done in this case.
1609 Close positive and negative detections will optionally be merged into
1611 Sky sources, or forced detections in background regions, will optionally
1612 be added, and the configured measurement algorithm will be run on all
1615 ConfigClass = DetectAndMeasureScoreConfig
1616 _DefaultName =
"detectAndMeasureScore"
1619 def run(self, science, matchedTemplate, difference, scoreExposure, kernelSources,
1620 idFactory=None, measurementResults=None):
1621 """Detect and measure sources on a score image.
1625 science : `lsst.afw.image.ExposureF`
1626 Science exposure that the template was subtracted from.
1627 matchedTemplate : `lsst.afw.image.ExposureF`
1628 Warped and PSF-matched template that was used produce the
1630 difference : `lsst.afw.image.ExposureF`
1631 Result of subtracting template from the science image.
1632 scoreExposure : `lsst.afw.image.ExposureF`
1633 Score or maximum likelihood difference image
1634 kernelSources : `lsst.afw.table.SourceCatalog`
1635 Final selection of sources that was used for psf matching.
1636 idFactory : `lsst.afw.table.IdFactory`, optional
1637 Generator object used to assign ids to detected sources in the
1638 difference image. Ids from this generator are not set until after
1639 deblending and merging positive/negative peaks.
1640 measurementResults : `lsst.pipe.base.Struct`, optional
1641 Result struct that is modified to allow saving of partial outputs
1642 for some failure conditions. If the task completes successfully,
1643 this is also returned.
1647 measurementResults : `lsst.pipe.base.Struct`
1649 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
1650 Subtracted exposure with detection mask applied.
1651 ``scoreMeasuredExposure`` : `lsst.afw.image.ExposureF`
1652 Score exposure with detection mask applied.
1653 ``diaSources`` : `lsst.afw.table.SourceCatalog`
1654 The catalog of detected sources.
1655 ``differenceBackground`` : `lsst.afw.math.BackgroundList`
1656 Background that was subtracted from the difference image.
1658 if measurementResults
is None:
1659 measurementResults = pipeBase.Struct()
1660 if idFactory
is None:
1661 idFactory = lsst.meas.base.IdGenerator().make_table_id_factory()
1664 self._prepareInputs(difference)
1665 self._prepareInputs(scoreExposure)
1666 if self.config.doSubtractBackground:
1667 background = self._fitAndSubtractBackground(
1668 differenceExposure=difference, scoreExposure=scoreExposure,
1671 background = afwMath.BackgroundList()
1673 if self.config.doFindCosmicRays:
1676 self.findAndMaskCosmicRays(difference)
1677 scoreExposure.mask.array |= difference.mask.array
1682 table = afwTable.SourceTable.make(self.schema)
1683 detectResults = self.detection.run(
1685 exposure=scoreExposure,
1687 background=background,
1690 measurementResults.differenceBackground = background
1691 measurementResults.scoreMeasuredExposure = scoreExposure
1696 detectedBit = difference.mask.getPlaneBitMask([
"DETECTED"])
1697 detectedPix = scoreExposure.mask.array & detectedBit > 0
1698 detectedNegativeBit = difference.mask.getPlaneBitMask([
"DETECTED_NEGATIVE"])
1699 detectedNegativePix = scoreExposure.mask.array & detectedNegativeBit > 0
1700 difference.mask.array[detectedPix] |= detectedBit
1701 difference.mask.array[detectedNegativePix] |= detectedNegativeBit
1703 edgeBit = difference.mask.getPlaneBitMask([
"EDGE"])
1704 edgePix = scoreExposure.mask.array & edgeBit > 0
1705 difference.mask.array[edgePix] |= edgeBit
1707 if self.config.doDeblend:
1708 sources, positives, negatives = self._deblend(difference,
1709 detectResults.positive,
1710 detectResults.negative)
1712 self.processResults(science, matchedTemplate, difference,
1713 sources, idFactory, kernelSources,
1714 positives=positives,
1715 negatives=negatives,
1716 measurementResults=measurementResults)
1719 positives = afwTable.SourceCatalog(self.schema)
1720 detectResults.positive.makeSources(positives)
1721 negatives = afwTable.SourceCatalog(self.schema)
1722 detectResults.negative.makeSources(negatives)
1723 self.processResults(science, matchedTemplate, difference,
1724 detectResults.sources, idFactory, kernelSources,
1725 positives=positives,
1726 negatives=negatives,
1727 measurementResults=measurementResults)
1728 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)