22from deprecated.sphinx
import deprecated
26from lsst.meas.algorithms
import SkyObjectsTask, SourceDetectionTask
27from lsst.meas.base import ForcedMeasurementTask, ApplyApCorrTask, DetectorVisitIdGeneratorConfig
28import lsst.meas.extensions.trailedSources
29import lsst.meas.extensions.shapeHSM
30from lsst.obs.base
import ExposureIdInfo
34from lsst.utils.timer
import timeMethod
36from .
import DipoleFitTask
38__all__ = [
"DetectAndMeasureConfig",
"DetectAndMeasureTask",
39 "DetectAndMeasureScoreConfig",
"DetectAndMeasureScoreTask"]
43 dimensions=(
"instrument",
"visit",
"detector"),
44 defaultTemplates={
"coaddName":
"deep",
47 science = pipeBase.connectionTypes.Input(
48 doc=
"Input science exposure.",
49 dimensions=(
"instrument",
"visit",
"detector"),
50 storageClass=
"ExposureF",
51 name=
"{fakesType}calexp"
53 matchedTemplate = pipeBase.connectionTypes.Input(
54 doc=
"Warped and PSF-matched template used to create the difference image.",
55 dimensions=(
"instrument",
"visit",
"detector"),
56 storageClass=
"ExposureF",
57 name=
"{fakesType}{coaddName}Diff_matchedExp",
59 difference = pipeBase.connectionTypes.Input(
60 doc=
"Result of subtracting template from science.",
61 dimensions=(
"instrument",
"visit",
"detector"),
62 storageClass=
"ExposureF",
63 name=
"{fakesType}{coaddName}Diff_differenceTempExp",
65 outputSchema = pipeBase.connectionTypes.InitOutput(
66 doc=
"Schema (as an example catalog) for output DIASource catalog.",
67 storageClass=
"SourceCatalog",
68 name=
"{fakesType}{coaddName}Diff_diaSrc_schema",
70 diaSources = pipeBase.connectionTypes.Output(
71 doc=
"Detected diaSources on the difference image.",
72 dimensions=(
"instrument",
"visit",
"detector"),
73 storageClass=
"SourceCatalog",
74 name=
"{fakesType}{coaddName}Diff_diaSrc",
76 subtractedMeasuredExposure = pipeBase.connectionTypes.Output(
77 doc=
"Difference image with detection mask plane filled in.",
78 dimensions=(
"instrument",
"visit",
"detector"),
79 storageClass=
"ExposureF",
80 name=
"{fakesType}{coaddName}Diff_differenceExp",
84class DetectAndMeasureConfig(pipeBase.PipelineTaskConfig,
85 pipelineConnections=DetectAndMeasureConnections):
86 """Config for DetectAndMeasureTask
88 doMerge = pexConfig.Field(
91 doc=
"Merge positive and negative diaSources with grow radius "
92 "set by growFootprint"
94 doForcedMeasurement = pexConfig.Field(
97 doc=
"Force photometer diaSource locations on PVI?")
98 doAddMetrics = pexConfig.Field(
101 doc=
"Add columns to the source table to hold analysis metrics?"
103 detection = pexConfig.ConfigurableField(
104 target=SourceDetectionTask,
105 doc=
"Final source detection for diaSource measurement",
107 measurement = pexConfig.ConfigurableField(
108 target=DipoleFitTask,
109 doc=
"Task to measure sources on the difference image.",
111 doApCorr = lsst.pex.config.Field(
114 doc=
"Run subtask to apply aperture corrections"
116 applyApCorr = lsst.pex.config.ConfigurableField(
117 target=ApplyApCorrTask,
118 doc=
"Task to apply aperture corrections"
120 forcedMeasurement = pexConfig.ConfigurableField(
121 target=ForcedMeasurementTask,
122 doc=
"Task to force photometer science image at diaSource locations.",
124 growFootprint = pexConfig.Field(
127 doc=
"Grow positive and negative footprints by this many pixels before merging"
129 diaSourceMatchRadius = pexConfig.Field(
132 doc=
"Match radius (in arcseconds) for DiaSource to Source association"
134 doSkySources = pexConfig.Field(
137 doc=
"Generate sky sources?",
139 skySources = pexConfig.ConfigurableField(
140 target=SkyObjectsTask,
141 doc=
"Generate sky sources",
143 idGenerator = DetectorVisitIdGeneratorConfig.make_field()
145 def setDefaults(self):
147 self.detection.thresholdPolarity =
"both"
148 self.detection.thresholdValue = 5.0
149 self.detection.reEstimateBackground =
False
150 self.detection.thresholdType =
"pixel_stdev"
153 self.measurement.algorithms.names.add(
'base_PeakLikelihoodFlux')
154 self.measurement.plugins.names |= [
'ext_trailedSources_Naive',
155 'base_LocalPhotoCalib',
157 'ext_shapeHSM_HsmSourceMoments',
158 'ext_shapeHSM_HsmPsfMoments',
160 self.measurement.slots.psfShape =
"ext_shapeHSM_HsmPsfMoments"
161 self.measurement.slots.shape =
"ext_shapeHSM_HsmSourceMoments"
163 self.forcedMeasurement.plugins = [
"base_TransformedCentroid",
"base_PsfFlux"]
164 self.forcedMeasurement.copyColumns = {
165 "id":
"objectId",
"parent":
"parentObjectId",
"coord_ra":
"coord_ra",
"coord_dec":
"coord_dec"}
166 self.forcedMeasurement.slots.centroid =
"base_TransformedCentroid"
167 self.forcedMeasurement.slots.shape =
None
170class DetectAndMeasureTask(lsst.pipe.base.PipelineTask):
171 """Detect and measure sources on a difference image.
173 ConfigClass = DetectAndMeasureConfig
174 _DefaultName = "detectAndMeasure"
176 def __init__(self, **kwargs):
177 super().__init__(**kwargs)
178 self.schema = afwTable.SourceTable.makeMinimalSchema()
181 self.makeSubtask(
"detection", schema=self.schema)
182 self.makeSubtask(
"measurement", schema=self.schema,
183 algMetadata=self.algMetadata)
184 if self.config.doApCorr:
185 self.makeSubtask(
"applyApCorr", schema=self.measurement.schema)
186 if self.config.doForcedMeasurement:
187 self.schema.addField(
188 "ip_diffim_forced_PsfFlux_instFlux",
"D",
189 "Forced PSF flux measured on the direct image.",
191 self.schema.addField(
192 "ip_diffim_forced_PsfFlux_instFluxErr",
"D",
193 "Forced PSF flux error measured on the direct image.",
195 self.schema.addField(
196 "ip_diffim_forced_PsfFlux_area",
"F",
197 "Forced PSF flux effective area of PSF.",
199 self.schema.addField(
200 "ip_diffim_forced_PsfFlux_flag",
"Flag",
201 "Forced PSF flux general failure flag.")
202 self.schema.addField(
203 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
"Flag",
204 "Forced PSF flux not enough non-rejected pixels in data to attempt the fit.")
205 self.schema.addField(
206 "ip_diffim_forced_PsfFlux_flag_edge",
"Flag",
207 "Forced PSF flux object was too close to the edge of the image to use the full PSF model.")
208 self.makeSubtask(
"forcedMeasurement", refSchema=self.schema)
210 self.schema.addField(
"refMatchId",
"L",
"unique id of reference catalog match")
211 self.schema.addField(
"srcMatchId",
"L",
"unique id of source match")
212 if self.config.doSkySources:
213 self.makeSubtask(
"skySources")
214 self.skySourceKey = self.schema.addField(
"sky_source", type=
"Flag", doc=
"Sky objects.")
217 self.outputSchema = afwTable.SourceCatalog(self.schema)
218 self.outputSchema.getTable().setMetadata(self.algMetadata)
223 "ID factory construction now depends on configuration; use the "
224 "idGenerator config field. Will be removed after v27."
227 category=FutureWarning,
229 def makeIdFactory(expId, expBits):
230 """Create IdFactory instance for unique 64 bit diaSource id-s.
238 Number of used bits in ``expId``.
242 The diasource id-s consists of the ``expId`` stored fixed
in the highest value
243 ``expBits`` of the 64-bit integer plus (bitwise
or) a generated sequence number
in the
244 low value end of the integer.
250 return ExposureIdInfo(expId, expBits).makeSourceIdFactory()
252 def runQuantum(self, butlerQC: pipeBase.ButlerQuantumContext,
253 inputRefs: pipeBase.InputQuantizedConnection,
254 outputRefs: pipeBase.OutputQuantizedConnection):
255 inputs = butlerQC.get(inputRefs)
256 idGenerator = self.config.idGenerator.apply(butlerQC.quantum.dataId)
257 idFactory = idGenerator.make_table_id_factory()
258 outputs = self.run(**inputs, idFactory=idFactory)
259 butlerQC.put(outputs, outputRefs)
262 def run(self, science, matchedTemplate, difference,
264 """Detect and measure sources on a difference image.
266 The difference image will be convolved with a gaussian approximation of
267 the PSF to form a maximum likelihood image
for detection.
268 Close positive
and negative detections will optionally be merged into
270 Sky sources,
or forced detections
in background regions, will optionally
271 be added,
and the configured measurement algorithm will be run on all
276 science : `lsst.afw.image.ExposureF`
277 Science exposure that the template was subtracted
from.
278 matchedTemplate : `lsst.afw.image.ExposureF`
279 Warped
and PSF-matched template that was used produce the
281 difference : `lsst.afw.image.ExposureF`
282 Result of subtracting template
from the science image.
284 Generator object to assign ids to detected sources
in the difference image.
288 measurementResults : `lsst.pipe.base.Struct`
290 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
291 Subtracted exposure
with detection mask applied.
293 The catalog of detected sources.
296 mask = difference.mask
297 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
299 table = afwTable.SourceTable.make(self.schema, idFactory)
300 table.setMetadata(self.algMetadata)
301 results = self.detection.
run(
307 return self.processResults(science, matchedTemplate, difference, results.sources, table,
308 positiveFootprints=results.positive, negativeFootprints=results.negative)
310 def processResults(self, science, matchedTemplate, difference, sources, table,
311 positiveFootprints=None, negativeFootprints=None,):
312 """Measure and process the results of source detection.
317 Detected sources on the difference exposure.
319 Positive polarity footprints.
321 Negative polarity footprints.
323 Table object that will be used to create the SourceCatalog.
324 science : `lsst.afw.image.ExposureF`
325 Science exposure that the template was subtracted from.
326 matchedTemplate : `lsst.afw.image.ExposureF`
327 Warped
and PSF-matched template that was used produce the
329 difference : `lsst.afw.image.ExposureF`
330 Result of subtracting template
from the science image.
334 measurementResults : `lsst.pipe.base.Struct`
336 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
337 Subtracted exposure
with detection mask applied.
339 The catalog of detected sources.
341 if self.config.doMerge:
342 fpSet = positiveFootprints
343 fpSet.merge(negativeFootprints, self.config.growFootprint,
344 self.config.growFootprint,
False)
345 diaSources = afwTable.SourceCatalog(table)
346 fpSet.makeSources(diaSources)
347 self.log.info(
"Merging detections into %d sources", len(diaSources))
351 if self.config.doSkySources:
352 self.addSkySources(diaSources, difference.mask, difference.info.id)
354 self.measureDiaSources(diaSources, science, difference, matchedTemplate)
356 if self.config.doForcedMeasurement:
357 self.measureForcedSources(diaSources, science, difference.getWcs())
359 measurementResults = pipeBase.Struct(
360 subtractedMeasuredExposure=difference,
361 diaSources=diaSources,
364 return measurementResults
366 def addSkySources(self, diaSources, mask, seed):
367 """Add sources in empty regions of the difference image
368 for measuring the background.
373 The catalog of detected sources.
375 Mask plane
for determining regions where Sky sources can be added.
377 Seed value to initialize the random number generator.
379 skySourceFootprints = self.skySources.run(mask=mask, seed=seed)
380 if skySourceFootprints:
381 for foot
in skySourceFootprints:
382 s = diaSources.addNew()
384 s.set(self.skySourceKey,
True)
386 def measureDiaSources(self, diaSources, science, difference, matchedTemplate):
387 """Use (matched) template and science image to constrain dipole fitting.
392 The catalog of detected sources.
393 science : `lsst.afw.image.ExposureF`
394 Science exposure that the template was subtracted from.
395 difference : `lsst.afw.image.ExposureF`
396 Result of subtracting template
from the science image.
397 matchedTemplate : `lsst.afw.image.ExposureF`
398 Warped
and PSF-matched template that was used produce the
403 self.measurement.
run(diaSources, difference, science, matchedTemplate)
404 if self.config.doApCorr:
405 self.applyApCorr.
run(
407 apCorrMap=difference.getInfo().getApCorrMap()
410 def measureForcedSources(self, diaSources, science, wcs):
411 """Perform forced measurement of the diaSources on the science image.
416 The catalog of detected sources.
417 science : `lsst.afw.image.ExposureF`
418 Science exposure that the template was subtracted from.
420 Coordinate system definition (wcs)
for the exposure.
424 forcedSources = self.forcedMeasurement.generateMeasCat(
425 science, diaSources, wcs)
426 self.forcedMeasurement.
run(forcedSources, science, diaSources, wcs)
427 mapper = afwTable.SchemaMapper(forcedSources.schema, diaSources.schema)
428 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFlux")[0],
429 "ip_diffim_forced_PsfFlux_instFlux",
True)
430 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFluxErr")[0],
431 "ip_diffim_forced_PsfFlux_instFluxErr",
True)
432 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_area")[0],
433 "ip_diffim_forced_PsfFlux_area",
True)
434 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag")[0],
435 "ip_diffim_forced_PsfFlux_flag",
True)
436 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_noGoodPixels")[0],
437 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
True)
438 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_edge")[0],
439 "ip_diffim_forced_PsfFlux_flag_edge",
True)
440 for diaSource, forcedSource
in zip(diaSources, forcedSources):
441 diaSource.assign(forcedSource, mapper)
445 scoreExposure = pipeBase.connectionTypes.Input(
446 doc=
"Maximum likelihood image for detection.",
447 dimensions=(
"instrument",
"visit",
"detector"),
448 storageClass=
"ExposureF",
449 name=
"{fakesType}{coaddName}Diff_scoreExp",
453class DetectAndMeasureScoreConfig(DetectAndMeasureConfig,
454 pipelineConnections=DetectAndMeasureScoreConnections):
458class DetectAndMeasureScoreTask(DetectAndMeasureTask):
459 """Detect DIA sources using a score image,
460 and measure the detections on the difference image.
462 Source detection
is run on the supplied score,
or maximum likelihood,
463 image. Note that no additional convolution will be done
in this case.
464 Close positive
and negative detections will optionally be merged into
466 Sky sources,
or forced detections
in background regions, will optionally
467 be added,
and the configured measurement algorithm will be run on all
470 ConfigClass = DetectAndMeasureScoreConfig
471 _DefaultName = "detectAndMeasureScore"
474 def run(self, science, matchedTemplate, difference, scoreExposure,
476 """Detect and measure sources on a score image.
480 science : `lsst.afw.image.ExposureF`
481 Science exposure that the template was subtracted from.
482 matchedTemplate : `lsst.afw.image.ExposureF`
483 Warped
and PSF-matched template that was used produce the
485 difference : `lsst.afw.image.ExposureF`
486 Result of subtracting template
from the science image.
487 scoreExposure : `lsst.afw.image.ExposureF`
488 Score
or maximum likelihood difference image
490 Generator object to assign ids to detected sources
in the difference image.
494 measurementResults : `lsst.pipe.base.Struct`
496 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
497 Subtracted exposure
with detection mask applied.
499 The catalog of detected sources.
502 mask = scoreExposure.mask
503 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
505 table = afwTable.SourceTable.make(self.schema, idFactory)
506 table.setMetadata(self.algMetadata)
510 goodBBox = scoreExposure.getPsf().getKernel().shrinkBBox(scoreExposure.getBBox())
511 results = self.detection.
run(
513 exposure=scoreExposure[goodBBox],
517 difference.mask.assign(scoreExposure.mask, scoreExposure.getBBox())
519 return self.processResults(science, matchedTemplate, difference, results.sources, table,
520 positiveFootprints=results.positive, negativeFootprints=results.negative)
def run(self, coaddExposures, bbox, wcs, dataIds, physical_filter=None, **kwargs)