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"
151 self.detection.excludeMaskPlanes = [
"EDGE"]
154 self.measurement.algorithms.names.add(
'base_PeakLikelihoodFlux')
155 self.measurement.plugins.names |= [
'ext_trailedSources_Naive',
156 'base_LocalPhotoCalib',
158 'ext_shapeHSM_HsmSourceMoments',
159 'ext_shapeHSM_HsmPsfMoments',
161 self.measurement.slots.psfShape =
"ext_shapeHSM_HsmPsfMoments"
162 self.measurement.slots.shape =
"ext_shapeHSM_HsmSourceMoments"
163 self.measurement.plugins[
"base_NaiveCentroid"].maxDistToPeak = 5.0
164 self.measurement.plugins[
"base_SdssCentroid"].maxDistToPeak = 5.0
165 self.forcedMeasurement.plugins = [
"base_TransformedCentroid",
"base_PsfFlux"]
166 self.forcedMeasurement.copyColumns = {
167 "id":
"objectId",
"parent":
"parentObjectId",
"coord_ra":
"coord_ra",
"coord_dec":
"coord_dec"}
168 self.forcedMeasurement.slots.centroid =
"base_TransformedCentroid"
169 self.forcedMeasurement.slots.shape =
None
172class DetectAndMeasureTask(lsst.pipe.base.PipelineTask):
173 """Detect and measure sources on a difference image.
175 ConfigClass = DetectAndMeasureConfig
176 _DefaultName = "detectAndMeasure"
178 def __init__(self, **kwargs):
179 super().__init__(**kwargs)
180 self.schema = afwTable.SourceTable.makeMinimalSchema()
183 self.makeSubtask(
"detection", schema=self.schema)
184 self.makeSubtask(
"measurement", schema=self.schema,
185 algMetadata=self.algMetadata)
186 if self.config.doApCorr:
187 self.makeSubtask(
"applyApCorr", schema=self.measurement.schema)
188 if self.config.doForcedMeasurement:
189 self.schema.addField(
190 "ip_diffim_forced_PsfFlux_instFlux",
"D",
191 "Forced PSF flux measured on the direct image.",
193 self.schema.addField(
194 "ip_diffim_forced_PsfFlux_instFluxErr",
"D",
195 "Forced PSF flux error measured on the direct image.",
197 self.schema.addField(
198 "ip_diffim_forced_PsfFlux_area",
"F",
199 "Forced PSF flux effective area of PSF.",
201 self.schema.addField(
202 "ip_diffim_forced_PsfFlux_flag",
"Flag",
203 "Forced PSF flux general failure flag.")
204 self.schema.addField(
205 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
"Flag",
206 "Forced PSF flux not enough non-rejected pixels in data to attempt the fit.")
207 self.schema.addField(
208 "ip_diffim_forced_PsfFlux_flag_edge",
"Flag",
209 "Forced PSF flux object was too close to the edge of the image to use the full PSF model.")
210 self.makeSubtask(
"forcedMeasurement", refSchema=self.schema)
212 self.schema.addField(
"refMatchId",
"L",
"unique id of reference catalog match")
213 self.schema.addField(
"srcMatchId",
"L",
"unique id of source match")
214 if self.config.doSkySources:
215 self.makeSubtask(
"skySources")
216 self.skySourceKey = self.schema.addField(
"sky_source", type=
"Flag", doc=
"Sky objects.")
219 self.outputSchema = afwTable.SourceCatalog(self.schema)
220 self.outputSchema.getTable().setMetadata(self.algMetadata)
225 "ID factory construction now depends on configuration; use the "
226 "idGenerator config field. Will be removed after v27."
229 category=FutureWarning,
231 def makeIdFactory(expId, expBits):
232 """Create IdFactory instance for unique 64 bit diaSource id-s.
240 Number of used bits in ``expId``.
244 The diasource id-s consists of the ``expId`` stored fixed
in the highest value
245 ``expBits`` of the 64-bit integer plus (bitwise
or) a generated sequence number
in the
246 low value end of the integer.
252 return ExposureIdInfo(expId, expBits).makeSourceIdFactory()
254 def runQuantum(self, butlerQC: pipeBase.ButlerQuantumContext,
255 inputRefs: pipeBase.InputQuantizedConnection,
256 outputRefs: pipeBase.OutputQuantizedConnection):
257 inputs = butlerQC.get(inputRefs)
258 idGenerator = self.config.idGenerator.apply(butlerQC.quantum.dataId)
259 idFactory = idGenerator.make_table_id_factory()
260 outputs = self.run(**inputs, idFactory=idFactory)
261 butlerQC.put(outputs, outputRefs)
264 def run(self, science, matchedTemplate, difference,
266 """Detect and measure sources on a difference image.
268 The difference image will be convolved with a gaussian approximation of
269 the PSF to form a maximum likelihood image
for detection.
270 Close positive
and negative detections will optionally be merged into
272 Sky sources,
or forced detections
in background regions, will optionally
273 be added,
and the configured measurement algorithm will be run on all
278 science : `lsst.afw.image.ExposureF`
279 Science exposure that the template was subtracted
from.
280 matchedTemplate : `lsst.afw.image.ExposureF`
281 Warped
and PSF-matched template that was used produce the
283 difference : `lsst.afw.image.ExposureF`
284 Result of subtracting template
from the science image.
286 Generator object to assign ids to detected sources
in the difference image.
290 measurementResults : `lsst.pipe.base.Struct`
292 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
293 Subtracted exposure
with detection mask applied.
295 The catalog of detected sources.
298 mask = difference.mask
299 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
301 table = afwTable.SourceTable.make(self.schema, idFactory)
302 table.setMetadata(self.algMetadata)
303 results = self.detection.
run(
309 return self.processResults(science, matchedTemplate, difference, results.sources, table,
310 positiveFootprints=results.positive, negativeFootprints=results.negative)
312 def processResults(self, science, matchedTemplate, difference, sources, table,
313 positiveFootprints=None, negativeFootprints=None,):
314 """Measure and process the results of source detection.
319 Detected sources on the difference exposure.
321 Positive polarity footprints.
323 Negative polarity footprints.
325 Table object that will be used to create the SourceCatalog.
326 science : `lsst.afw.image.ExposureF`
327 Science exposure that the template was subtracted from.
328 matchedTemplate : `lsst.afw.image.ExposureF`
329 Warped
and PSF-matched template that was used produce the
331 difference : `lsst.afw.image.ExposureF`
332 Result of subtracting template
from the science image.
336 measurementResults : `lsst.pipe.base.Struct`
338 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
339 Subtracted exposure
with detection mask applied.
341 The catalog of detected sources.
343 if self.config.doMerge:
344 fpSet = positiveFootprints
345 fpSet.merge(negativeFootprints, self.config.growFootprint,
346 self.config.growFootprint,
False)
347 diaSources = afwTable.SourceCatalog(table)
348 fpSet.makeSources(diaSources)
349 self.log.info(
"Merging detections into %d sources", len(diaSources))
353 if self.config.doSkySources:
354 self.addSkySources(diaSources, difference.mask, difference.info.id)
356 self.measureDiaSources(diaSources, science, difference, matchedTemplate)
358 if self.config.doForcedMeasurement:
359 self.measureForcedSources(diaSources, science, difference.getWcs())
361 measurementResults = pipeBase.Struct(
362 subtractedMeasuredExposure=difference,
363 diaSources=diaSources,
366 return measurementResults
368 def addSkySources(self, diaSources, mask, seed):
369 """Add sources in empty regions of the difference image
370 for measuring the background.
375 The catalog of detected sources.
377 Mask plane
for determining regions where Sky sources can be added.
379 Seed value to initialize the random number generator.
381 skySourceFootprints = self.skySources.run(mask=mask, seed=seed)
382 if skySourceFootprints:
383 for foot
in skySourceFootprints:
384 s = diaSources.addNew()
386 s.set(self.skySourceKey,
True)
388 def measureDiaSources(self, diaSources, science, difference, matchedTemplate):
389 """Use (matched) template and science image to constrain dipole fitting.
394 The catalog of detected sources.
395 science : `lsst.afw.image.ExposureF`
396 Science exposure that the template was subtracted from.
397 difference : `lsst.afw.image.ExposureF`
398 Result of subtracting template
from the science image.
399 matchedTemplate : `lsst.afw.image.ExposureF`
400 Warped
and PSF-matched template that was used produce the
405 self.measurement.
run(diaSources, difference, science, matchedTemplate)
406 if self.config.doApCorr:
407 self.applyApCorr.
run(
409 apCorrMap=difference.getInfo().getApCorrMap()
412 def measureForcedSources(self, diaSources, science, wcs):
413 """Perform forced measurement of the diaSources on the science image.
418 The catalog of detected sources.
419 science : `lsst.afw.image.ExposureF`
420 Science exposure that the template was subtracted from.
422 Coordinate system definition (wcs)
for the exposure.
426 forcedSources = self.forcedMeasurement.generateMeasCat(
427 science, diaSources, wcs)
428 self.forcedMeasurement.
run(forcedSources, science, diaSources, wcs)
429 mapper = afwTable.SchemaMapper(forcedSources.schema, diaSources.schema)
430 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFlux")[0],
431 "ip_diffim_forced_PsfFlux_instFlux",
True)
432 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFluxErr")[0],
433 "ip_diffim_forced_PsfFlux_instFluxErr",
True)
434 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_area")[0],
435 "ip_diffim_forced_PsfFlux_area",
True)
436 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag")[0],
437 "ip_diffim_forced_PsfFlux_flag",
True)
438 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_noGoodPixels")[0],
439 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
True)
440 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_edge")[0],
441 "ip_diffim_forced_PsfFlux_flag_edge",
True)
442 for diaSource, forcedSource
in zip(diaSources, forcedSources):
443 diaSource.assign(forcedSource, mapper)
447 scoreExposure = pipeBase.connectionTypes.Input(
448 doc=
"Maximum likelihood image for detection.",
449 dimensions=(
"instrument",
"visit",
"detector"),
450 storageClass=
"ExposureF",
451 name=
"{fakesType}{coaddName}Diff_scoreExp",
455class DetectAndMeasureScoreConfig(DetectAndMeasureConfig,
456 pipelineConnections=DetectAndMeasureScoreConnections):
460class DetectAndMeasureScoreTask(DetectAndMeasureTask):
461 """Detect DIA sources using a score image,
462 and measure the detections on the difference image.
464 Source detection
is run on the supplied score,
or maximum likelihood,
465 image. Note that no additional convolution will be done
in this case.
466 Close positive
and negative detections will optionally be merged into
468 Sky sources,
or forced detections
in background regions, will optionally
469 be added,
and the configured measurement algorithm will be run on all
472 ConfigClass = DetectAndMeasureScoreConfig
473 _DefaultName = "detectAndMeasureScore"
476 def run(self, science, matchedTemplate, difference, scoreExposure,
478 """Detect and measure sources on a score image.
482 science : `lsst.afw.image.ExposureF`
483 Science exposure that the template was subtracted from.
484 matchedTemplate : `lsst.afw.image.ExposureF`
485 Warped
and PSF-matched template that was used produce the
487 difference : `lsst.afw.image.ExposureF`
488 Result of subtracting template
from the science image.
489 scoreExposure : `lsst.afw.image.ExposureF`
490 Score
or maximum likelihood difference image
492 Generator object to assign ids to detected sources
in the difference image.
496 measurementResults : `lsst.pipe.base.Struct`
498 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
499 Subtracted exposure
with detection mask applied.
501 The catalog of detected sources.
504 mask = scoreExposure.mask
505 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
507 table = afwTable.SourceTable.make(self.schema, idFactory)
508 table.setMetadata(self.algMetadata)
509 results = self.detection.
run(
511 exposure=scoreExposure,
515 difference.mask.assign(scoreExposure.mask, scoreExposure.getBBox())
517 return self.processResults(science, matchedTemplate, difference, results.sources, table,
518 positiveFootprints=results.positive, negativeFootprints=results.negative)
def run(self, coaddExposures, bbox, wcs, dataIds, physical_filter=None, **kwargs)