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"]
40 dimensions=(
"instrument",
"visit",
"detector"),
41 defaultTemplates={
"coaddName":
"deep",
44 science = pipeBase.connectionTypes.Input(
45 doc=
"Input science exposure.",
46 dimensions=(
"instrument",
"visit",
"detector"),
47 storageClass=
"ExposureF",
48 name=
"{fakesType}calexp"
50 matchedTemplate = pipeBase.connectionTypes.Input(
51 doc=
"Warped and PSF-matched template used to create the difference image.",
52 dimensions=(
"instrument",
"visit",
"detector"),
53 storageClass=
"ExposureF",
54 name=
"{fakesType}{coaddName}Diff_matchedExp",
56 difference = pipeBase.connectionTypes.Input(
57 doc=
"Result of subtracting template from science.",
58 dimensions=(
"instrument",
"visit",
"detector"),
59 storageClass=
"ExposureF",
60 name=
"{fakesType}{coaddName}Diff_differenceTempExp",
62 outputSchema = pipeBase.connectionTypes.InitOutput(
63 doc=
"Schema (as an example catalog) for output DIASource catalog.",
64 storageClass=
"SourceCatalog",
65 name=
"{fakesType}{coaddName}Diff_diaSrc_schema",
67 diaSources = pipeBase.connectionTypes.Output(
68 doc=
"Detected diaSources on the difference image.",
69 dimensions=(
"instrument",
"visit",
"detector"),
70 storageClass=
"SourceCatalog",
71 name=
"{fakesType}{coaddName}Diff_diaSrc",
73 subtractedMeasuredExposure = pipeBase.connectionTypes.Output(
74 doc=
"Difference image with detection mask plane filled in.",
75 dimensions=(
"instrument",
"visit",
"detector"),
76 storageClass=
"ExposureF",
77 name=
"{fakesType}{coaddName}Diff_differenceExp",
81class DetectAndMeasureConfig(pipeBase.PipelineTaskConfig,
82 pipelineConnections=DetectAndMeasureConnections):
83 """Config for DetectAndMeasureTask
85 doMerge = pexConfig.Field(
88 doc=
"Merge positive and negative diaSources with grow radius "
89 "set by growFootprint"
91 doForcedMeasurement = pexConfig.Field(
94 doc=
"Force photometer diaSource locations on PVI?")
95 doAddMetrics = pexConfig.Field(
98 doc=
"Add columns to the source table to hold analysis metrics?"
100 detection = pexConfig.ConfigurableField(
101 target=SourceDetectionTask,
102 doc=
"Final source detection for diaSource measurement",
104 measurement = pexConfig.ConfigurableField(
105 target=DipoleFitTask,
106 doc=
"Task to measure sources on the difference image.",
108 doApCorr = lsst.pex.config.Field(
111 doc=
"Run subtask to apply aperture corrections"
113 applyApCorr = lsst.pex.config.ConfigurableField(
114 target=ApplyApCorrTask,
115 doc=
"Task to apply aperture corrections"
117 forcedMeasurement = pexConfig.ConfigurableField(
118 target=ForcedMeasurementTask,
119 doc=
"Task to force photometer science image at diaSource locations.",
121 growFootprint = pexConfig.Field(
124 doc=
"Grow positive and negative footprints by this many pixels before merging"
126 diaSourceMatchRadius = pexConfig.Field(
129 doc=
"Match radius (in arcseconds) for DiaSource to Source association"
131 doSkySources = pexConfig.Field(
134 doc=
"Generate sky sources?",
136 skySources = pexConfig.ConfigurableField(
137 target=SkyObjectsTask,
138 doc=
"Generate sky sources",
141 def setDefaults(self):
143 self.detection.thresholdPolarity =
"both"
144 self.detection.thresholdValue = 5.0
145 self.detection.reEstimateBackground =
False
146 self.detection.thresholdType =
"pixel_stdev"
149 self.measurement.algorithms.names.add(
'base_PeakLikelihoodFlux')
150 self.measurement.plugins.names |= [
'ext_trailedSources_Naive',
151 'base_LocalPhotoCalib',
153 'ext_shapeHSM_HsmSourceMoments',
154 'ext_shapeHSM_HsmPsfMoments',
156 self.measurement.slots.psfShape =
"ext_shapeHSM_HsmPsfMoments"
157 self.measurement.slots.shape =
"ext_shapeHSM_HsmSourceMoments"
159 self.forcedMeasurement.plugins = [
"base_TransformedCentroid",
"base_PsfFlux"]
160 self.forcedMeasurement.copyColumns = {
161 "id":
"objectId",
"parent":
"parentObjectId",
"coord_ra":
"coord_ra",
"coord_dec":
"coord_dec"}
162 self.forcedMeasurement.slots.centroid =
"base_TransformedCentroid"
163 self.forcedMeasurement.slots.shape =
None
166class DetectAndMeasureTask(lsst.pipe.base.PipelineTask):
167 """Detect and measure sources on a difference image.
169 ConfigClass = DetectAndMeasureConfig
170 _DefaultName = "detectAndMeasure"
172 def __init__(self, **kwargs):
173 super().__init__(**kwargs)
174 self.schema = afwTable.SourceTable.makeMinimalSchema()
177 self.makeSubtask(
"detection", schema=self.schema)
178 self.makeSubtask(
"measurement", schema=self.schema,
179 algMetadata=self.algMetadata)
180 if self.config.doApCorr:
181 self.makeSubtask(
"applyApCorr", schema=self.measurement.schema)
182 if self.config.doForcedMeasurement:
183 self.schema.addField(
184 "ip_diffim_forced_PsfFlux_instFlux",
"D",
185 "Forced PSF flux measured on the direct image.",
187 self.schema.addField(
188 "ip_diffim_forced_PsfFlux_instFluxErr",
"D",
189 "Forced PSF flux error measured on the direct image.",
191 self.schema.addField(
192 "ip_diffim_forced_PsfFlux_area",
"F",
193 "Forced PSF flux effective area of PSF.",
195 self.schema.addField(
196 "ip_diffim_forced_PsfFlux_flag",
"Flag",
197 "Forced PSF flux general failure flag.")
198 self.schema.addField(
199 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
"Flag",
200 "Forced PSF flux not enough non-rejected pixels in data to attempt the fit.")
201 self.schema.addField(
202 "ip_diffim_forced_PsfFlux_flag_edge",
"Flag",
203 "Forced PSF flux object was too close to the edge of the image to use the full PSF model.")
204 self.makeSubtask(
"forcedMeasurement", refSchema=self.schema)
206 self.schema.addField(
"refMatchId",
"L",
"unique id of reference catalog match")
207 self.schema.addField(
"srcMatchId",
"L",
"unique id of source match")
208 if self.config.doSkySources:
209 self.makeSubtask(
"skySources")
210 self.skySourceKey = self.schema.addField(
"sky_source", type=
"Flag", doc=
"Sky objects.")
213 self.outputSchema = afwTable.SourceCatalog(self.schema)
214 self.outputSchema.getTable().setMetadata(self.algMetadata)
217 def makeIdFactory(expId, expBits):
218 """Create IdFactory instance for unique 64 bit diaSource id-s.
226 Number of used bits in ``expId``.
230 The diasource id-s consists of the ``expId`` stored fixed
in the highest value
231 ``expBits`` of the 64-bit integer plus (bitwise
or) a generated sequence number
in the
232 low value end of the integer.
238 return ExposureIdInfo(expId, expBits).makeSourceIdFactory()
240 def runQuantum(self, butlerQC: pipeBase.ButlerQuantumContext,
241 inputRefs: pipeBase.InputQuantizedConnection,
242 outputRefs: pipeBase.OutputQuantizedConnection):
243 inputs = butlerQC.get(inputRefs)
244 expId, expBits = butlerQC.quantum.dataId.pack(
"visit_detector",
246 idFactory = self.makeIdFactory(expId=expId, expBits=expBits)
248 outputs = self.run(inputs[
'science'],
249 inputs[
'matchedTemplate'],
250 inputs[
'difference'],
252 butlerQC.put(outputs, outputRefs)
255 def run(self, science, matchedTemplate, difference,
257 """Detect and measure sources on a difference image.
261 science : `lsst.afw.image.ExposureF`
262 Science exposure that the template was subtracted from.
263 matchedTemplate : `lsst.afw.image.ExposureF`
264 Warped
and PSF-matched template that was used produce the
266 difference : `lsst.afw.image.ExposureF`
267 Result of subtracting template
from the science image.
269 Generator object to assign ids to detected sources
in the difference image.
273 results : `lsst.pipe.base.Struct`
275 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
276 Subtracted exposure
with detection mask applied.
278 The catalog of detected sources.
281 mask = difference.mask
282 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
284 table = afwTable.SourceTable.make(self.schema, idFactory)
285 table.setMetadata(self.algMetadata)
286 results = self.detection.
run(
292 if self.config.doMerge:
293 fpSet = results.fpSets.positive
294 fpSet.merge(results.fpSets.negative, self.config.growFootprint,
295 self.config.growFootprint,
False)
296 diaSources = afwTable.SourceCatalog(table)
297 fpSet.makeSources(diaSources)
298 self.log.info(
"Merging detections into %d sources", len(diaSources))
300 diaSources = results.sources
302 if self.config.doSkySources:
303 self.addSkySources(diaSources, difference.mask, difference.info.id)
305 self.measureDiaSources(diaSources, science, difference, matchedTemplate)
307 if self.config.doForcedMeasurement:
308 self.measureForcedSources(diaSources, science, difference.getWcs())
310 return pipeBase.Struct(
311 subtractedMeasuredExposure=difference,
312 diaSources=diaSources,
315 def addSkySources(self, diaSources, mask, seed):
316 """Add sources in empty regions of the difference image
317 for measuring the background.
322 The catalog of detected sources.
324 Mask plane
for determining regions where Sky sources can be added.
326 Seed value to initialize the random number generator.
328 skySourceFootprints = self.skySources.run(mask=mask, seed=seed)
329 if skySourceFootprints:
330 for foot
in skySourceFootprints:
331 s = diaSources.addNew()
333 s.set(self.skySourceKey,
True)
335 def measureDiaSources(self, diaSources, science, difference, matchedTemplate):
336 """Use (matched) template and science image to constrain dipole fitting.
341 The catalog of detected sources.
342 science : `lsst.afw.image.ExposureF`
343 Science exposure that the template was subtracted from.
344 difference : `lsst.afw.image.ExposureF`
345 Result of subtracting template
from the science image.
346 matchedTemplate : `lsst.afw.image.ExposureF`
347 Warped
and PSF-matched template that was used produce the
352 self.measurement.
run(diaSources, difference, science, matchedTemplate)
353 if self.config.doApCorr:
354 self.applyApCorr.
run(
356 apCorrMap=difference.getInfo().getApCorrMap()
359 def measureForcedSources(self, diaSources, science, wcs):
360 """Perform forced measurement of the diaSources on the science image.
365 The catalog of detected sources.
366 science : `lsst.afw.image.ExposureF`
367 Science exposure that the template was subtracted from.
369 Coordinate system definition (wcs)
for the exposure.
373 forcedSources = self.forcedMeasurement.generateMeasCat(
374 science, diaSources, wcs)
375 self.forcedMeasurement.
run(forcedSources, science, diaSources, wcs)
376 mapper = afwTable.SchemaMapper(forcedSources.schema, diaSources.schema)
377 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFlux")[0],
378 "ip_diffim_forced_PsfFlux_instFlux",
True)
379 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFluxErr")[0],
380 "ip_diffim_forced_PsfFlux_instFluxErr",
True)
381 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_area")[0],
382 "ip_diffim_forced_PsfFlux_area",
True)
383 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag")[0],
384 "ip_diffim_forced_PsfFlux_flag",
True)
385 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_noGoodPixels")[0],
386 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
True)
387 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_edge")[0],
388 "ip_diffim_forced_PsfFlux_flag_edge",
True)
389 for diaSource, forcedSource
in zip(diaSources, forcedSources):
390 diaSource.assign(forcedSource, mapper)
def run(self, coaddExposures, bbox, wcs, dataIds, **kwargs)