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",
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
172 self.measurement.plugins[
'base_PixelFlags'].masksFpAnywhere = [
'STREAK']
173 self.measurement.plugins[
'base_PixelFlags'].masksFpCenter = [
'STREAK']
177 """Detect and measure sources on a difference image.
179 ConfigClass = DetectAndMeasureConfig
180 _DefaultName =
"detectAndMeasure"
182 def __init__(self, **kwargs):
183 super().__init__(**kwargs)
184 self.schema = afwTable.SourceTable.makeMinimalSchema()
186 afwTable.CoordKey.addErrorFields(self.schema)
189 self.makeSubtask(
"detection", schema=self.schema)
190 self.makeSubtask(
"measurement", schema=self.schema,
191 algMetadata=self.algMetadata)
192 if self.config.doApCorr:
193 self.makeSubtask(
"applyApCorr", schema=self.measurement.schema)
194 if self.config.doForcedMeasurement:
195 self.schema.addField(
196 "ip_diffim_forced_PsfFlux_instFlux",
"D",
197 "Forced PSF flux measured on the direct image.",
199 self.schema.addField(
200 "ip_diffim_forced_PsfFlux_instFluxErr",
"D",
201 "Forced PSF flux error measured on the direct image.",
203 self.schema.addField(
204 "ip_diffim_forced_PsfFlux_area",
"F",
205 "Forced PSF flux effective area of PSF.",
207 self.schema.addField(
208 "ip_diffim_forced_PsfFlux_flag",
"Flag",
209 "Forced PSF flux general failure flag.")
210 self.schema.addField(
211 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
"Flag",
212 "Forced PSF flux not enough non-rejected pixels in data to attempt the fit.")
213 self.schema.addField(
214 "ip_diffim_forced_PsfFlux_flag_edge",
"Flag",
215 "Forced PSF flux object was too close to the edge of the image to use the full PSF model.")
216 self.makeSubtask(
"forcedMeasurement", refSchema=self.schema)
218 self.schema.addField(
"refMatchId",
"L",
"unique id of reference catalog match")
219 self.schema.addField(
"srcMatchId",
"L",
"unique id of source match")
220 if self.config.doSkySources:
221 self.makeSubtask(
"skySources")
222 self.skySourceKey = self.schema.addField(
"sky_source", type=
"Flag", doc=
"Sky objects.")
225 self.outputSchema = afwTable.SourceCatalog(self.schema)
226 self.outputSchema.getTable().setMetadata(self.algMetadata)
232 "ID factory construction now depends on configuration; use the "
233 "idGenerator config field. Will be removed after v26."
236 category=FutureWarning,
239 """Create IdFactory instance for unique 64 bit diaSource id-s.
247 Number of used bits in ``expId``.
251 The diasource id-s consists of the ``expId`` stored fixed in the highest value
252 ``expBits`` of the 64-bit integer plus (bitwise or) a generated sequence number in the
253 low value end of the integer.
257 idFactory: `lsst.afw.table.IdFactory`
261 def runQuantum(self, butlerQC: pipeBase.ButlerQuantumContext,
262 inputRefs: pipeBase.InputQuantizedConnection,
263 outputRefs: pipeBase.OutputQuantizedConnection):
264 inputs = butlerQC.get(inputRefs)
265 idGenerator = self.config.idGenerator.apply(butlerQC.quantum.dataId)
266 idFactory = idGenerator.make_table_id_factory()
267 outputs = self.run(**inputs, idFactory=idFactory)
268 butlerQC.put(outputs, outputRefs)
271 def run(self, science, matchedTemplate, difference,
273 """Detect and measure sources on a difference image.
275 The difference image will be convolved with a gaussian approximation of
276 the PSF to form a maximum likelihood image for detection.
277 Close positive and negative detections will optionally be merged into
279 Sky sources, or forced detections in background regions, will optionally
280 be added, and the configured measurement algorithm will be run on all
285 science : `lsst.afw.image.ExposureF`
286 Science exposure that the template was subtracted from.
287 matchedTemplate : `lsst.afw.image.ExposureF`
288 Warped and PSF-matched template that was used produce the
290 difference : `lsst.afw.image.ExposureF`
291 Result of subtracting template from the science image.
292 idFactory : `lsst.afw.table.IdFactory`, optional
293 Generator object to assign ids to detected sources in the difference image.
297 measurementResults : `lsst.pipe.base.Struct`
299 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
300 Subtracted exposure with detection mask applied.
301 ``diaSources`` : `lsst.afw.table.SourceCatalog`
302 The catalog of detected sources.
305 mask = difference.mask
306 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
308 table = afwTable.SourceTable.make(self.schema, idFactory)
309 table.setMetadata(self.algMetadata)
310 results = self.detection.
run(
316 return self.processResults(science, matchedTemplate, difference, results.sources, table,
317 positiveFootprints=results.positive, negativeFootprints=results.negative)
319 def processResults(self, science, matchedTemplate, difference, sources, table,
320 positiveFootprints=None, negativeFootprints=None,):
321 """Measure and process the results of source detection.
325 sources : `lsst.afw.table.SourceCatalog`
326 Detected sources on the difference exposure.
327 positiveFootprints : `lsst.afw.detection.FootprintSet`, optional
328 Positive polarity footprints.
329 negativeFootprints : `lsst.afw.detection.FootprintSet`, optional
330 Negative polarity footprints.
331 table : `lsst.afw.table.SourceTable`
332 Table object that will be used to create the SourceCatalog.
333 science : `lsst.afw.image.ExposureF`
334 Science exposure that the template was subtracted from.
335 matchedTemplate : `lsst.afw.image.ExposureF`
336 Warped and PSF-matched template that was used produce the
338 difference : `lsst.afw.image.ExposureF`
339 Result of subtracting template from the science image.
343 measurementResults : `lsst.pipe.base.Struct`
345 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
346 Subtracted exposure with detection mask applied.
347 ``diaSources`` : `lsst.afw.table.SourceCatalog`
348 The catalog of detected sources.
350 if self.config.doMerge:
351 fpSet = positiveFootprints
352 fpSet.merge(negativeFootprints, self.config.growFootprint,
353 self.config.growFootprint,
False)
354 diaSources = afwTable.SourceCatalog(table)
355 fpSet.makeSources(diaSources)
356 self.log.
info(
"Merging detections into %d sources",
len(diaSources))
360 if self.config.doSkySources:
361 self.addSkySources(diaSources, difference.mask, difference.info.id)
363 self.measureDiaSources(diaSources, science, difference, matchedTemplate)
365 if self.config.doForcedMeasurement:
366 self.measureForcedSources(diaSources, science, difference.getWcs())
368 measurementResults = pipeBase.Struct(
369 subtractedMeasuredExposure=difference,
370 diaSources=diaSources,
373 return measurementResults
376 """Add sources in empty regions of the difference image
377 for measuring the background.
381 diaSources : `lsst.afw.table.SourceCatalog`
382 The catalog of detected sources.
383 mask : `lsst.afw.image.Mask`
384 Mask plane for determining regions where Sky sources can be added.
386 Seed value to initialize the random number generator.
388 skySourceFootprints = self.skySources.
run(mask=mask, seed=seed)
389 if skySourceFootprints:
390 for foot
in skySourceFootprints:
391 s = diaSources.addNew()
393 s.set(self.skySourceKey,
True)
396 """Use (matched) template and science image to constrain dipole fitting.
400 diaSources : `lsst.afw.table.SourceCatalog`
401 The catalog of detected sources.
402 science : `lsst.afw.image.ExposureF`
403 Science exposure that the template was subtracted from.
404 difference : `lsst.afw.image.ExposureF`
405 Result of subtracting template from the science image.
406 matchedTemplate : `lsst.afw.image.ExposureF`
407 Warped and PSF-matched template that was used produce the
412 self.measurement.
run(diaSources, difference, science, matchedTemplate)
413 if self.config.doApCorr:
414 apCorrMap = difference.getInfo().getApCorrMap()
415 if apCorrMap
is None:
416 self.log.
warning(
"Difference image does not have valid aperture correction; skipping.")
418 self.applyApCorr.
run(
424 """Perform forced measurement of the diaSources on the science image.
428 diaSources : `lsst.afw.table.SourceCatalog`
429 The catalog of detected sources.
430 science : `lsst.afw.image.ExposureF`
431 Science exposure that the template was subtracted from.
432 wcs : `lsst.afw.geom.SkyWcs`
433 Coordinate system definition (wcs) for the exposure.
437 forcedSources = self.forcedMeasurement.generateMeasCat(
438 science, diaSources, wcs)
439 self.forcedMeasurement.
run(forcedSources, science, diaSources, wcs)
440 mapper = afwTable.SchemaMapper(forcedSources.schema, diaSources.schema)
441 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFlux")[0],
442 "ip_diffim_forced_PsfFlux_instFlux",
True)
443 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFluxErr")[0],
444 "ip_diffim_forced_PsfFlux_instFluxErr",
True)
445 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_area")[0],
446 "ip_diffim_forced_PsfFlux_area",
True)
447 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag")[0],
448 "ip_diffim_forced_PsfFlux_flag",
True)
449 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_noGoodPixels")[0],
450 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
True)
451 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_edge")[0],
452 "ip_diffim_forced_PsfFlux_flag_edge",
True)
453 for diaSource, forcedSource
in zip(diaSources, forcedSources):
454 diaSource.assign(forcedSource, mapper)
458 scoreExposure = pipeBase.connectionTypes.Input(
459 doc=
"Maximum likelihood image for detection.",
460 dimensions=(
"instrument",
"visit",
"detector"),
461 storageClass=
"ExposureF",
462 name=
"{fakesType}{coaddName}Diff_scoreExp",
467 pipelineConnections=DetectAndMeasureScoreConnections):
472 """Detect DIA sources using a score image,
473 and measure the detections on the difference image.
475 Source detection is run on the supplied score, or maximum likelihood,
476 image. Note that no additional convolution will be done in this case.
477 Close positive and negative detections will optionally be merged into
479 Sky sources, or forced detections in background regions, will optionally
480 be added, and the configured measurement algorithm will be run on all
483 ConfigClass = DetectAndMeasureScoreConfig
484 _DefaultName =
"detectAndMeasureScore"
487 def run(self, science, matchedTemplate, difference, scoreExposure,
489 """Detect and measure sources on a score image.
493 science : `lsst.afw.image.ExposureF`
494 Science exposure that the template was subtracted from.
495 matchedTemplate : `lsst.afw.image.ExposureF`
496 Warped and PSF-matched template that was used produce the
498 difference : `lsst.afw.image.ExposureF`
499 Result of subtracting template from the science image.
500 scoreExposure : `lsst.afw.image.ExposureF`
501 Score or maximum likelihood difference image
502 idFactory : `lsst.afw.table.IdFactory`, optional
503 Generator object to assign ids to detected sources in the difference image.
507 measurementResults : `lsst.pipe.base.Struct`
509 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
510 Subtracted exposure with detection mask applied.
511 ``diaSources`` : `lsst.afw.table.SourceCatalog`
512 The catalog of detected sources.
515 mask = scoreExposure.mask
516 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
518 table = afwTable.SourceTable.make(self.schema, idFactory)
519 table.setMetadata(self.algMetadata)
520 results = self.detection.
run(
522 exposure=scoreExposure,
526 difference.mask.assign(scoreExposure.mask, scoreExposure.getBBox())
528 return self.processResults(science, matchedTemplate, difference, results.sources, table,
529 positiveFootprints=results.positive, negativeFootprints=results.negative)
Asseses the quality of a candidate given a spatial kernel and background model.
run(self, coaddExposures, bbox, wcs, dataIds, physical_filter=None, **kwargs)