24from lsst.meas.algorithms
import SkyObjectsTask, SourceDetectionTask
26import lsst.meas.extensions.trailedSources
27import lsst.meas.extensions.shapeHSM
28from lsst.obs.base
import ExposureIdInfo
32from lsst.utils.timer
import timeMethod
34from .
import DipoleFitTask
36__all__ = [
"DetectAndMeasureConfig",
"DetectAndMeasureTask",
37 "DetectAndMeasureScoreConfig",
"DetectAndMeasureScoreTask"]
41 dimensions=(
"instrument",
"visit",
"detector"),
42 defaultTemplates={
"coaddName":
"deep",
45 science = pipeBase.connectionTypes.Input(
46 doc=
"Input science exposure.",
47 dimensions=(
"instrument",
"visit",
"detector"),
48 storageClass=
"ExposureF",
49 name=
"{fakesType}calexp"
51 matchedTemplate = pipeBase.connectionTypes.Input(
52 doc=
"Warped and PSF-matched template used to create the difference image.",
53 dimensions=(
"instrument",
"visit",
"detector"),
54 storageClass=
"ExposureF",
55 name=
"{fakesType}{coaddName}Diff_matchedExp",
57 difference = pipeBase.connectionTypes.Input(
58 doc=
"Result of subtracting template from science.",
59 dimensions=(
"instrument",
"visit",
"detector"),
60 storageClass=
"ExposureF",
61 name=
"{fakesType}{coaddName}Diff_differenceTempExp",
63 outputSchema = pipeBase.connectionTypes.InitOutput(
64 doc=
"Schema (as an example catalog) for output DIASource catalog.",
65 storageClass=
"SourceCatalog",
66 name=
"{fakesType}{coaddName}Diff_diaSrc_schema",
68 diaSources = pipeBase.connectionTypes.Output(
69 doc=
"Detected diaSources on the difference image.",
70 dimensions=(
"instrument",
"visit",
"detector"),
71 storageClass=
"SourceCatalog",
72 name=
"{fakesType}{coaddName}Diff_diaSrc",
74 subtractedMeasuredExposure = pipeBase.connectionTypes.Output(
75 doc=
"Difference image with detection mask plane filled in.",
76 dimensions=(
"instrument",
"visit",
"detector"),
77 storageClass=
"ExposureF",
78 name=
"{fakesType}{coaddName}Diff_differenceExp",
82class DetectAndMeasureConfig(pipeBase.PipelineTaskConfig,
83 pipelineConnections=DetectAndMeasureConnections):
84 """Config for DetectAndMeasureTask
86 doMerge = pexConfig.Field(
89 doc=
"Merge positive and negative diaSources with grow radius "
90 "set by growFootprint"
92 doForcedMeasurement = pexConfig.Field(
95 doc=
"Force photometer diaSource locations on PVI?")
96 doAddMetrics = pexConfig.Field(
99 doc=
"Add columns to the source table to hold analysis metrics?"
101 detection = pexConfig.ConfigurableField(
102 target=SourceDetectionTask,
103 doc=
"Final source detection for diaSource measurement",
105 measurement = pexConfig.ConfigurableField(
106 target=DipoleFitTask,
107 doc=
"Task to measure sources on the difference image.",
109 doApCorr = lsst.pex.config.Field(
112 doc=
"Run subtask to apply aperture corrections"
114 applyApCorr = lsst.pex.config.ConfigurableField(
115 target=ApplyApCorrTask,
116 doc=
"Task to apply aperture corrections"
118 forcedMeasurement = pexConfig.ConfigurableField(
119 target=ForcedMeasurementTask,
120 doc=
"Task to force photometer science image at diaSource locations.",
122 growFootprint = pexConfig.Field(
125 doc=
"Grow positive and negative footprints by this many pixels before merging"
127 diaSourceMatchRadius = pexConfig.Field(
130 doc=
"Match radius (in arcseconds) for DiaSource to Source association"
132 doSkySources = pexConfig.Field(
135 doc=
"Generate sky sources?",
137 skySources = pexConfig.ConfigurableField(
138 target=SkyObjectsTask,
139 doc=
"Generate sky sources",
142 def setDefaults(self):
144 self.detection.thresholdPolarity =
"both"
145 self.detection.thresholdValue = 5.0
146 self.detection.reEstimateBackground =
False
147 self.detection.thresholdType =
"pixel_stdev"
150 self.measurement.algorithms.names.add(
'base_PeakLikelihoodFlux')
151 self.measurement.plugins.names |= [
'ext_trailedSources_Naive',
152 'base_LocalPhotoCalib',
154 'ext_shapeHSM_HsmSourceMoments',
155 'ext_shapeHSM_HsmPsfMoments',
157 self.measurement.slots.psfShape =
"ext_shapeHSM_HsmPsfMoments"
158 self.measurement.slots.shape =
"ext_shapeHSM_HsmSourceMoments"
160 self.forcedMeasurement.plugins = [
"base_TransformedCentroid",
"base_PsfFlux"]
161 self.forcedMeasurement.copyColumns = {
162 "id":
"objectId",
"parent":
"parentObjectId",
"coord_ra":
"coord_ra",
"coord_dec":
"coord_dec"}
163 self.forcedMeasurement.slots.centroid =
"base_TransformedCentroid"
164 self.forcedMeasurement.slots.shape =
None
167class DetectAndMeasureTask(lsst.pipe.base.PipelineTask):
168 """Detect and measure sources on a difference image.
170 ConfigClass = DetectAndMeasureConfig
171 _DefaultName = "detectAndMeasure"
173 def __init__(self, **kwargs):
174 super().__init__(**kwargs)
175 self.schema = afwTable.SourceTable.makeMinimalSchema()
178 self.makeSubtask(
"detection", schema=self.schema)
179 self.makeSubtask(
"measurement", schema=self.schema,
180 algMetadata=self.algMetadata)
181 if self.config.doApCorr:
182 self.makeSubtask(
"applyApCorr", schema=self.measurement.schema)
183 if self.config.doForcedMeasurement:
184 self.schema.addField(
185 "ip_diffim_forced_PsfFlux_instFlux",
"D",
186 "Forced PSF flux measured on the direct image.",
188 self.schema.addField(
189 "ip_diffim_forced_PsfFlux_instFluxErr",
"D",
190 "Forced PSF flux error measured on the direct image.",
192 self.schema.addField(
193 "ip_diffim_forced_PsfFlux_area",
"F",
194 "Forced PSF flux effective area of PSF.",
196 self.schema.addField(
197 "ip_diffim_forced_PsfFlux_flag",
"Flag",
198 "Forced PSF flux general failure flag.")
199 self.schema.addField(
200 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
"Flag",
201 "Forced PSF flux not enough non-rejected pixels in data to attempt the fit.")
202 self.schema.addField(
203 "ip_diffim_forced_PsfFlux_flag_edge",
"Flag",
204 "Forced PSF flux object was too close to the edge of the image to use the full PSF model.")
205 self.makeSubtask(
"forcedMeasurement", refSchema=self.schema)
207 self.schema.addField(
"refMatchId",
"L",
"unique id of reference catalog match")
208 self.schema.addField(
"srcMatchId",
"L",
"unique id of source match")
209 if self.config.doSkySources:
210 self.makeSubtask(
"skySources")
211 self.skySourceKey = self.schema.addField(
"sky_source", type=
"Flag", doc=
"Sky objects.")
214 self.outputSchema = afwTable.SourceCatalog(self.schema)
215 self.outputSchema.getTable().setMetadata(self.algMetadata)
218 def makeIdFactory(expId, expBits):
219 """Create IdFactory instance for unique 64 bit diaSource id-s.
227 Number of used bits in ``expId``.
231 The diasource id-s consists of the ``expId`` stored fixed
in the highest value
232 ``expBits`` of the 64-bit integer plus (bitwise
or) a generated sequence number
in the
233 low value end of the integer.
239 return ExposureIdInfo(expId, expBits).makeSourceIdFactory()
241 def runQuantum(self, butlerQC: pipeBase.ButlerQuantumContext,
242 inputRefs: pipeBase.InputQuantizedConnection,
243 outputRefs: pipeBase.OutputQuantizedConnection):
244 inputs = butlerQC.get(inputRefs)
245 expId, expBits = butlerQC.quantum.dataId.pack(
"visit_detector",
247 idFactory = self.makeIdFactory(expId=expId, expBits=expBits)
249 outputs = self.run(**inputs, idFactory=idFactory)
250 butlerQC.put(outputs, outputRefs)
253 def run(self, science, matchedTemplate, difference,
255 """Detect and measure sources on a difference image.
257 The difference image will be convolved with a gaussian approximation of
258 the PSF to form a maximum likelihood image
for detection.
259 Close positive
and negative detections will optionally be merged into
261 Sky sources,
or forced detections
in background regions, will optionally
262 be added,
and the configured measurement algorithm will be run on all
267 science : `lsst.afw.image.ExposureF`
268 Science exposure that the template was subtracted
from.
269 matchedTemplate : `lsst.afw.image.ExposureF`
270 Warped
and PSF-matched template that was used produce the
272 difference : `lsst.afw.image.ExposureF`
273 Result of subtracting template
from the science image.
275 Generator object to assign ids to detected sources
in the difference image.
279 measurementResults : `lsst.pipe.base.Struct`
281 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
282 Subtracted exposure
with detection mask applied.
284 The catalog of detected sources.
287 mask = difference.mask
288 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
290 table = afwTable.SourceTable.make(self.schema, idFactory)
291 table.setMetadata(self.algMetadata)
292 results = self.detection.
run(
298 return self.processResults(science, matchedTemplate, difference, results.sources, table,
299 positiveFootprints=results.positive, negativeFootprints=results.negative)
301 def processResults(self, science, matchedTemplate, difference, sources, table,
302 positiveFootprints=None, negativeFootprints=None,):
303 """Measure and process the results of source detection.
308 Detected sources on the difference exposure.
310 Positive polarity footprints.
312 Negative polarity footprints.
314 Table object that will be used to create the SourceCatalog.
315 science : `lsst.afw.image.ExposureF`
316 Science exposure that the template was subtracted from.
317 matchedTemplate : `lsst.afw.image.ExposureF`
318 Warped
and PSF-matched template that was used produce the
320 difference : `lsst.afw.image.ExposureF`
321 Result of subtracting template
from the science image.
325 measurementResults : `lsst.pipe.base.Struct`
327 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
328 Subtracted exposure
with detection mask applied.
330 The catalog of detected sources.
332 if self.config.doMerge:
333 fpSet = positiveFootprints
334 fpSet.merge(negativeFootprints, self.config.growFootprint,
335 self.config.growFootprint,
False)
336 diaSources = afwTable.SourceCatalog(table)
337 fpSet.makeSources(diaSources)
338 self.log.info(
"Merging detections into %d sources", len(diaSources))
342 if self.config.doSkySources:
343 self.addSkySources(diaSources, difference.mask, difference.info.id)
345 self.measureDiaSources(diaSources, science, difference, matchedTemplate)
347 if self.config.doForcedMeasurement:
348 self.measureForcedSources(diaSources, science, difference.getWcs())
350 measurementResults = pipeBase.Struct(
351 subtractedMeasuredExposure=difference,
352 diaSources=diaSources,
355 return measurementResults
357 def addSkySources(self, diaSources, mask, seed):
358 """Add sources in empty regions of the difference image
359 for measuring the background.
364 The catalog of detected sources.
366 Mask plane
for determining regions where Sky sources can be added.
368 Seed value to initialize the random number generator.
370 skySourceFootprints = self.skySources.run(mask=mask, seed=seed)
371 if skySourceFootprints:
372 for foot
in skySourceFootprints:
373 s = diaSources.addNew()
375 s.set(self.skySourceKey,
True)
377 def measureDiaSources(self, diaSources, science, difference, matchedTemplate):
378 """Use (matched) template and science image to constrain dipole fitting.
383 The catalog of detected sources.
384 science : `lsst.afw.image.ExposureF`
385 Science exposure that the template was subtracted from.
386 difference : `lsst.afw.image.ExposureF`
387 Result of subtracting template
from the science image.
388 matchedTemplate : `lsst.afw.image.ExposureF`
389 Warped
and PSF-matched template that was used produce the
394 self.measurement.
run(diaSources, difference, science, matchedTemplate)
395 if self.config.doApCorr:
396 self.applyApCorr.
run(
398 apCorrMap=difference.getInfo().getApCorrMap()
401 def measureForcedSources(self, diaSources, science, wcs):
402 """Perform forced measurement of the diaSources on the science image.
407 The catalog of detected sources.
408 science : `lsst.afw.image.ExposureF`
409 Science exposure that the template was subtracted from.
411 Coordinate system definition (wcs)
for the exposure.
415 forcedSources = self.forcedMeasurement.generateMeasCat(
416 science, diaSources, wcs)
417 self.forcedMeasurement.
run(forcedSources, science, diaSources, wcs)
418 mapper = afwTable.SchemaMapper(forcedSources.schema, diaSources.schema)
419 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFlux")[0],
420 "ip_diffim_forced_PsfFlux_instFlux",
True)
421 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFluxErr")[0],
422 "ip_diffim_forced_PsfFlux_instFluxErr",
True)
423 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_area")[0],
424 "ip_diffim_forced_PsfFlux_area",
True)
425 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag")[0],
426 "ip_diffim_forced_PsfFlux_flag",
True)
427 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_noGoodPixels")[0],
428 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
True)
429 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_edge")[0],
430 "ip_diffim_forced_PsfFlux_flag_edge",
True)
431 for diaSource, forcedSource
in zip(diaSources, forcedSources):
432 diaSource.assign(forcedSource, mapper)
436 scoreExposure = pipeBase.connectionTypes.Input(
437 doc=
"Maximum likelihood image for detection.",
438 dimensions=(
"instrument",
"visit",
"detector"),
439 storageClass=
"ExposureF",
440 name=
"{fakesType}{coaddName}Diff_scoreExp",
444class DetectAndMeasureScoreConfig(DetectAndMeasureConfig,
445 pipelineConnections=DetectAndMeasureScoreConnections):
449class DetectAndMeasureScoreTask(DetectAndMeasureTask):
450 """Detect DIA sources using a score image,
451 and measure the detections on the difference image.
453 Source detection
is run on the supplied score,
or maximum likelihood,
454 image. Note that no additional convolution will be done
in this case.
455 Close positive
and negative detections will optionally be merged into
457 Sky sources,
or forced detections
in background regions, will optionally
458 be added,
and the configured measurement algorithm will be run on all
461 ConfigClass = DetectAndMeasureScoreConfig
462 _DefaultName = "detectAndMeasureScore"
465 def run(self, science, matchedTemplate, difference, scoreExposure,
467 """Detect and measure sources on a score image.
471 science : `lsst.afw.image.ExposureF`
472 Science exposure that the template was subtracted from.
473 matchedTemplate : `lsst.afw.image.ExposureF`
474 Warped
and PSF-matched template that was used produce the
476 difference : `lsst.afw.image.ExposureF`
477 Result of subtracting template
from the science image.
478 scoreExposure : `lsst.afw.image.ExposureF`
479 Score
or maximum likelihood difference image
481 Generator object to assign ids to detected sources
in the difference image.
485 measurementResults : `lsst.pipe.base.Struct`
487 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
488 Subtracted exposure
with detection mask applied.
490 The catalog of detected sources.
493 mask = scoreExposure.mask
494 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
496 table = afwTable.SourceTable.make(self.schema, idFactory)
497 table.setMetadata(self.algMetadata)
501 goodBBox = scoreExposure.getPsf().getKernel().shrinkBBox(scoreExposure.getBBox())
502 results = self.detection.
run(
504 exposure=scoreExposure[goodBBox],
508 difference.mask.assign(scoreExposure.mask, scoreExposure.getBBox())
510 return self.processResults(science, matchedTemplate, difference, results.sources, table,
511 positiveFootprints=results.positive, negativeFootprints=results.negative)
def run(self, coaddExposures, bbox, wcs, dataIds, **kwargs)