22from deprecated.sphinx
import deprecated
27from lsst.meas.base import ForcedMeasurementTask, ApplyApCorrTask, DetectorVisitIdGeneratorConfig
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.",
114 doc=
"Run subtask to apply aperture corrections"
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.")
220 self.outputSchema.getTable().setMetadata(self.algMetadata)
226 "ID factory construction now depends on configuration; use the "
227 "idGenerator config field. Will be removed after v26."
230 category=FutureWarning,
232 def makeIdFactory(expId, expBits):
233 """Create IdFactory instance for unique 64 bit diaSource id-s.
241 Number of used bits in ``expId``.
245 The diasource id-s consists of the ``expId`` stored fixed
in the highest value
246 ``expBits`` of the 64-bit integer plus (bitwise
or) a generated sequence number
in the
247 low value end of the integer.
253 return ExposureIdInfo(expId, expBits).makeSourceIdFactory()
255 def runQuantum(self, butlerQC: pipeBase.ButlerQuantumContext,
256 inputRefs: pipeBase.InputQuantizedConnection,
257 outputRefs: pipeBase.OutputQuantizedConnection):
258 inputs = butlerQC.get(inputRefs)
259 idGenerator = self.config.idGenerator.apply(butlerQC.quantum.dataId)
260 idFactory = idGenerator.make_table_id_factory()
261 outputs = self.run(**inputs, idFactory=idFactory)
262 butlerQC.put(outputs, outputRefs)
265 def run(self, science, matchedTemplate, difference,
267 """Detect and measure sources on a difference image.
269 The difference image will be convolved with a gaussian approximation of
270 the PSF to form a maximum likelihood image
for detection.
271 Close positive
and negative detections will optionally be merged into
273 Sky sources,
or forced detections
in background regions, will optionally
274 be added,
and the configured measurement algorithm will be run on all
279 science : `lsst.afw.image.ExposureF`
280 Science exposure that the template was subtracted
from.
281 matchedTemplate : `lsst.afw.image.ExposureF`
282 Warped
and PSF-matched template that was used produce the
284 difference : `lsst.afw.image.ExposureF`
285 Result of subtracting template
from the science image.
287 Generator object to assign ids to detected sources
in the difference image.
291 measurementResults : `lsst.pipe.base.Struct`
293 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
294 Subtracted exposure
with detection mask applied.
296 The catalog of detected sources.
299 mask = difference.mask
300 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
302 table = afwTable.SourceTable.make(self.schema, idFactory)
303 table.setMetadata(self.algMetadata)
304 results = self.detection.run(
310 return self.processResults(science, matchedTemplate, difference, results.sources, table,
311 positiveFootprints=results.positive, negativeFootprints=results.negative)
313 def processResults(self, science, matchedTemplate, difference, sources, table,
314 positiveFootprints=None, negativeFootprints=None,):
315 """Measure and process the results of source detection.
320 Detected sources on the difference exposure.
322 Positive polarity footprints.
324 Negative polarity footprints.
326 Table object that will be used to create the SourceCatalog.
327 science : `lsst.afw.image.ExposureF`
328 Science exposure that the template was subtracted from.
329 matchedTemplate : `lsst.afw.image.ExposureF`
330 Warped
and PSF-matched template that was used produce the
332 difference : `lsst.afw.image.ExposureF`
333 Result of subtracting template
from the science image.
337 measurementResults : `lsst.pipe.base.Struct`
339 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
340 Subtracted exposure
with detection mask applied.
342 The catalog of detected sources.
344 if self.config.doMerge:
345 fpSet = positiveFootprints
346 fpSet.merge(negativeFootprints, self.config.growFootprint,
347 self.config.growFootprint,
False)
349 fpSet.makeSources(diaSources)
350 self.log.info(
"Merging detections into %d sources", len(diaSources))
354 if self.config.doSkySources:
355 self.addSkySources(diaSources, difference.mask, difference.info.id)
357 self.measureDiaSources(diaSources, science, difference, matchedTemplate)
359 if self.config.doForcedMeasurement:
360 self.measureForcedSources(diaSources, science, difference.getWcs())
362 measurementResults = pipeBase.Struct(
363 subtractedMeasuredExposure=difference,
364 diaSources=diaSources,
367 return measurementResults
369 def addSkySources(self, diaSources, mask, seed):
370 """Add sources in empty regions of the difference image
371 for measuring the background.
376 The catalog of detected sources.
378 Mask plane
for determining regions where Sky sources can be added.
380 Seed value to initialize the random number generator.
382 skySourceFootprints = self.skySources.run(mask=mask, seed=seed)
383 if skySourceFootprints:
384 for foot
in skySourceFootprints:
385 s = diaSources.addNew()
387 s.set(self.skySourceKey,
True)
389 def measureDiaSources(self, diaSources, science, difference, matchedTemplate):
390 """Use (matched) template and science image to constrain dipole fitting.
395 The catalog of detected sources.
396 science : `lsst.afw.image.ExposureF`
397 Science exposure that the template was subtracted from.
398 difference : `lsst.afw.image.ExposureF`
399 Result of subtracting template
from the science image.
400 matchedTemplate : `lsst.afw.image.ExposureF`
401 Warped
and PSF-matched template that was used produce the
406 self.measurement.run(diaSources, difference, science, matchedTemplate)
407 if self.config.doApCorr:
408 apCorrMap = difference.getInfo().getApCorrMap()
409 if apCorrMap
is None:
410 self.log.warning(
"Difference image does not have valid aperture correction; skipping.")
412 self.applyApCorr.run(
417 def measureForcedSources(self, diaSources, science, wcs):
418 """Perform forced measurement of the diaSources on the science image.
423 The catalog of detected sources.
424 science : `lsst.afw.image.ExposureF`
425 Science exposure that the template was subtracted from.
427 Coordinate system definition (wcs)
for the exposure.
431 forcedSources = self.forcedMeasurement.generateMeasCat(
432 science, diaSources, wcs)
433 self.forcedMeasurement.run(forcedSources, science, diaSources, wcs)
435 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFlux")[0],
436 "ip_diffim_forced_PsfFlux_instFlux",
True)
437 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFluxErr")[0],
438 "ip_diffim_forced_PsfFlux_instFluxErr",
True)
439 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_area")[0],
440 "ip_diffim_forced_PsfFlux_area",
True)
441 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag")[0],
442 "ip_diffim_forced_PsfFlux_flag",
True)
443 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_noGoodPixels")[0],
444 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
True)
445 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_edge")[0],
446 "ip_diffim_forced_PsfFlux_flag_edge",
True)
447 for diaSource, forcedSource
in zip(diaSources, forcedSources):
448 diaSource.assign(forcedSource, mapper)
452 scoreExposure = pipeBase.connectionTypes.Input(
453 doc=
"Maximum likelihood image for detection.",
454 dimensions=(
"instrument",
"visit",
"detector"),
455 storageClass=
"ExposureF",
456 name=
"{fakesType}{coaddName}Diff_scoreExp",
460class DetectAndMeasureScoreConfig(DetectAndMeasureConfig,
461 pipelineConnections=DetectAndMeasureScoreConnections):
465class DetectAndMeasureScoreTask(DetectAndMeasureTask):
466 """Detect DIA sources using a score image,
467 and measure the detections on the difference image.
469 Source detection
is run on the supplied score,
or maximum likelihood,
470 image. Note that no additional convolution will be done
in this case.
471 Close positive
and negative detections will optionally be merged into
473 Sky sources,
or forced detections
in background regions, will optionally
474 be added,
and the configured measurement algorithm will be run on all
477 ConfigClass = DetectAndMeasureScoreConfig
478 _DefaultName = "detectAndMeasureScore"
481 def run(self, science, matchedTemplate, difference, scoreExposure,
483 """Detect and measure sources on a score image.
487 science : `lsst.afw.image.ExposureF`
488 Science exposure that the template was subtracted from.
489 matchedTemplate : `lsst.afw.image.ExposureF`
490 Warped
and PSF-matched template that was used produce the
492 difference : `lsst.afw.image.ExposureF`
493 Result of subtracting template
from the science image.
494 scoreExposure : `lsst.afw.image.ExposureF`
495 Score
or maximum likelihood difference image
497 Generator object to assign ids to detected sources
in the difference image.
501 measurementResults : `lsst.pipe.base.Struct`
503 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
504 Subtracted exposure
with detection mask applied.
506 The catalog of detected sources.
509 mask = scoreExposure.mask
510 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
512 table = afwTable.SourceTable.make(self.schema, idFactory)
513 table.setMetadata(self.algMetadata)
514 results = self.detection.run(
516 exposure=scoreExposure,
520 difference.mask.assign(scoreExposure.mask, scoreExposure.getBBox())
522 return self.processResults(science, matchedTemplate, difference, results.sources, table,
523 positiveFootprints=results.positive, negativeFootprints=results.negative)
A 2-dimensional celestial WCS that transform pixels to ICRS RA/Dec, using the LSST standard for pixel...
Represent a 2-dimensional array of bitmask pixels.
A polymorphic functor base class for generating record IDs for a table.
A mapping between the keys of two Schemas, used to copy data between them.
Table class that contains measurements made on a single exposure.
Class for storing ordered metadata with comments.