27from lsst.obs.base
import ExposureIdInfo
31from lsst.utils.timer
import timeMethod
33from .
import DipoleFitTask
35__all__ = [
"DetectAndMeasureConfig",
"DetectAndMeasureTask"]
39 dimensions=(
"instrument",
"visit",
"detector"),
40 defaultTemplates={
"coaddName":
"deep",
43 science = pipeBase.connectionTypes.Input(
44 doc=
"Input science exposure.",
45 dimensions=(
"instrument",
"visit",
"detector"),
46 storageClass=
"ExposureF",
47 name=
"{fakesType}calexp"
49 matchedTemplate = pipeBase.connectionTypes.Input(
50 doc=
"Warped and PSF-matched template used to create the difference image.",
51 dimensions=(
"instrument",
"visit",
"detector"),
52 storageClass=
"ExposureF",
53 name=
"{fakesType}{coaddName}Diff_matchedExp",
55 difference = pipeBase.connectionTypes.Input(
56 doc=
"Result of subtracting template from science.",
57 dimensions=(
"instrument",
"visit",
"detector"),
58 storageClass=
"ExposureF",
59 name=
"{fakesType}{coaddName}Diff_differenceTempExp",
61 outputSchema = pipeBase.connectionTypes.InitOutput(
62 doc=
"Schema (as an example catalog) for output DIASource catalog.",
63 storageClass=
"SourceCatalog",
64 name=
"{fakesType}{coaddName}Diff_diaSrc_schema",
66 diaSources = pipeBase.connectionTypes.Output(
67 doc=
"Detected diaSources on the difference image.",
68 dimensions=(
"instrument",
"visit",
"detector"),
69 storageClass=
"SourceCatalog",
70 name=
"{fakesType}{coaddName}Diff_diaSrc",
72 subtractedMeasuredExposure = pipeBase.connectionTypes.Output(
73 doc=
"Difference image with detection mask plane filled in.",
74 dimensions=(
"instrument",
"visit",
"detector"),
75 storageClass=
"ExposureF",
76 name=
"{fakesType}{coaddName}Diff_differenceExp",
80class DetectAndMeasureConfig(pipeBase.PipelineTaskConfig,
81 pipelineConnections=DetectAndMeasureConnections):
82 """Config for DetectAndMeasureTask
84 doMerge = pexConfig.Field(
87 doc=
"Merge positive and negative diaSources with grow radius "
88 "set by growFootprint"
90 doForcedMeasurement = pexConfig.Field(
93 doc=
"Force photometer diaSource locations on PVI?")
94 doAddMetrics = pexConfig.Field(
97 doc=
"Add columns to the source table to hold analysis metrics?"
99 detection = pexConfig.ConfigurableField(
100 target=SourceDetectionTask,
101 doc=
"Final source detection for diaSource measurement",
103 measurement = pexConfig.ConfigurableField(
104 target=DipoleFitTask,
105 doc=
"Task to measure sources on the difference image.",
110 doc=
"Run subtask to apply aperture corrections"
113 target=ApplyApCorrTask,
114 doc=
"Task to apply aperture corrections"
116 forcedMeasurement = pexConfig.ConfigurableField(
117 target=ForcedMeasurementTask,
118 doc=
"Task to force photometer science image at diaSource locations.",
120 growFootprint = pexConfig.Field(
123 doc=
"Grow positive and negative footprints by this many pixels before merging"
125 diaSourceMatchRadius = pexConfig.Field(
128 doc=
"Match radius (in arcseconds) for DiaSource to Source association"
130 doSkySources = pexConfig.Field(
133 doc=
"Generate sky sources?",
135 skySources = pexConfig.ConfigurableField(
136 target=SkyObjectsTask,
137 doc=
"Generate sky sources",
140 def setDefaults(self):
142 self.detection.thresholdPolarity =
"both"
143 self.detection.thresholdValue = 5.0
144 self.detection.reEstimateBackground =
False
145 self.detection.thresholdType =
"pixel_stdev"
148 self.measurement.algorithms.names.add(
'base_PeakLikelihoodFlux')
149 self.measurement.plugins.names |= [
'ext_trailedSources_Naive',
150 'base_LocalPhotoCalib',
153 self.forcedMeasurement.plugins = [
"base_TransformedCentroid",
"base_PsfFlux"]
154 self.forcedMeasurement.copyColumns = {
155 "id":
"objectId",
"parent":
"parentObjectId",
"coord_ra":
"coord_ra",
"coord_dec":
"coord_dec"}
156 self.forcedMeasurement.slots.centroid =
"base_TransformedCentroid"
157 self.forcedMeasurement.slots.shape =
None
160class DetectAndMeasureTask(lsst.pipe.base.PipelineTask):
161 """Detect and measure sources on a difference image.
163 ConfigClass = DetectAndMeasureConfig
164 _DefaultName = "detectAndMeasure"
166 def __init__(self, **kwargs):
167 super().__init__(**kwargs)
168 self.schema = afwTable.SourceTable.makeMinimalSchema()
171 self.makeSubtask(
"detection", schema=self.schema)
172 self.makeSubtask(
"measurement", schema=self.schema,
173 algMetadata=self.algMetadata)
174 if self.config.doApCorr:
175 self.makeSubtask(
"applyApCorr", schema=self.measurement.schema)
176 if self.config.doForcedMeasurement:
177 self.schema.addField(
178 "ip_diffim_forced_PsfFlux_instFlux",
"D",
179 "Forced PSF flux measured on the direct image.",
181 self.schema.addField(
182 "ip_diffim_forced_PsfFlux_instFluxErr",
"D",
183 "Forced PSF flux error measured on the direct image.",
185 self.schema.addField(
186 "ip_diffim_forced_PsfFlux_area",
"F",
187 "Forced PSF flux effective area of PSF.",
189 self.schema.addField(
190 "ip_diffim_forced_PsfFlux_flag",
"Flag",
191 "Forced PSF flux general failure flag.")
192 self.schema.addField(
193 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
"Flag",
194 "Forced PSF flux not enough non-rejected pixels in data to attempt the fit.")
195 self.schema.addField(
196 "ip_diffim_forced_PsfFlux_flag_edge",
"Flag",
197 "Forced PSF flux object was too close to the edge of the image to use the full PSF model.")
198 self.makeSubtask(
"forcedMeasurement", refSchema=self.schema)
200 self.schema.addField(
"refMatchId",
"L",
"unique id of reference catalog match")
201 self.schema.addField(
"srcMatchId",
"L",
"unique id of source match")
202 if self.config.doSkySources:
203 self.makeSubtask(
"skySources")
204 self.skySourceKey = self.schema.addField(
"sky_source", type=
"Flag", doc=
"Sky objects.")
208 self.outputSchema.getTable().setMetadata(self.algMetadata)
211 def makeIdFactory(expId, expBits):
212 """Create IdFactory instance for unique 64 bit diaSource id-s.
220 Number of used bits in ``expId``.
224 The diasource id-s consists of the ``expId`` stored fixed
in the highest value
225 ``expBits`` of the 64-bit integer plus (bitwise
or) a generated sequence number
in the
226 low value end of the integer.
232 return ExposureIdInfo(expId, expBits).makeSourceIdFactory()
234 def runQuantum(self, butlerQC: pipeBase.ButlerQuantumContext,
235 inputRefs: pipeBase.InputQuantizedConnection,
236 outputRefs: pipeBase.OutputQuantizedConnection):
237 inputs = butlerQC.get(inputRefs)
238 expId, expBits = butlerQC.quantum.dataId.pack(
"visit_detector",
240 idFactory = self.makeIdFactory(expId=expId, expBits=expBits)
242 outputs = self.run(inputs[
'science'],
243 inputs[
'matchedTemplate'],
244 inputs[
'difference'],
246 butlerQC.put(outputs, outputRefs)
249 def run(self, science, matchedTemplate, difference,
251 """Detect and measure sources on a difference image.
255 science : `lsst.afw.image.ExposureF`
256 Science exposure that the template was subtracted from.
257 matchedTemplate : `lsst.afw.image.ExposureF`
258 Warped
and PSF-matched template that was used produce the
260 difference : `lsst.afw.image.ExposureF`
261 Result of subtracting template
from the science image.
263 Generator object to assign ids to detected sources
in the difference image.
267 results : `lsst.pipe.base.Struct`
269 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
270 Subtracted exposure
with detection mask applied.
272 The catalog of detected sources.
275 mask = difference.mask
276 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
278 table = afwTable.SourceTable.make(self.schema, idFactory)
279 table.setMetadata(self.algMetadata)
280 results = self.detection.run(
286 if self.config.doMerge:
287 fpSet = results.fpSets.positive
288 fpSet.merge(results.fpSets.negative, self.config.growFootprint,
289 self.config.growFootprint,
False)
291 fpSet.makeSources(diaSources)
292 self.log.info(
"Merging detections into %d sources", len(diaSources))
294 diaSources = results.sources
296 if self.config.doSkySources:
297 self.addSkySources(diaSources, difference.mask, difference.info.id)
299 self.measureDiaSources(diaSources, science, difference, matchedTemplate)
301 if self.config.doForcedMeasurement:
302 self.measureForcedSources(diaSources, science, difference.getWcs())
304 return pipeBase.Struct(
305 subtractedMeasuredExposure=difference,
306 diaSources=diaSources,
309 def addSkySources(self, diaSources, mask, seed):
310 """Add sources in empty regions of the difference image
311 for measuring the background.
316 The catalog of detected sources.
318 Mask plane
for determining regions where Sky sources can be added.
320 Seed value to initialize the random number generator.
322 skySourceFootprints = self.skySources.run(mask=mask, seed=seed)
323 if skySourceFootprints:
324 for foot
in skySourceFootprints:
325 s = diaSources.addNew()
327 s.set(self.skySourceKey,
True)
329 def measureDiaSources(self, diaSources, science, difference, matchedTemplate):
330 """Use (matched) template and science image to constrain dipole fitting.
335 The catalog of detected sources.
336 science : `lsst.afw.image.ExposureF`
337 Science exposure that the template was subtracted from.
338 difference : `lsst.afw.image.ExposureF`
339 Result of subtracting template
from the science image.
340 matchedTemplate : `lsst.afw.image.ExposureF`
341 Warped
and PSF-matched template that was used produce the
346 self.measurement.run(diaSources, difference, science, matchedTemplate)
347 if self.config.doApCorr:
348 self.applyApCorr.run(
350 apCorrMap=difference.getInfo().getApCorrMap()
353 def measureForcedSources(self, diaSources, science, wcs):
354 """Perform forced measurement of the diaSources on the science image.
359 The catalog of detected sources.
360 science : `lsst.afw.image.ExposureF`
361 Science exposure that the template was subtracted from.
363 Coordinate system definition (wcs)
for the exposure.
367 forcedSources = self.forcedMeasurement.generateMeasCat(
368 science, diaSources, wcs)
369 self.forcedMeasurement.run(forcedSources, science, diaSources, wcs)
371 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFlux")[0],
372 "ip_diffim_forced_PsfFlux_instFlux",
True)
373 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFluxErr")[0],
374 "ip_diffim_forced_PsfFlux_instFluxErr",
True)
375 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_area")[0],
376 "ip_diffim_forced_PsfFlux_area",
True)
377 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag")[0],
378 "ip_diffim_forced_PsfFlux_flag",
True)
379 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_noGoodPixels")[0],
380 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
True)
381 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_edge")[0],
382 "ip_diffim_forced_PsfFlux_flag_edge",
True)
383 for diaSource, forcedSource
in zip(diaSources, forcedSources):
384 diaSource.assign(forcedSource, mapper)
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.
Class for storing ordered metadata with comments.