LSST Applications 28.0.0,g1653933729+a8ce1bb630,g1a997c3884+a8ce1bb630,g28da252d5a+5bd70b7e6d,g2bbee38e9b+638fca75ac,g2bc492864f+638fca75ac,g3156d2b45e+07302053f8,g347aa1857d+638fca75ac,g35bb328faa+a8ce1bb630,g3a166c0a6a+638fca75ac,g3e281a1b8c+7bbb0b2507,g4005a62e65+17cd334064,g414038480c+5b5cd4fff3,g41af890bb2+4ffae9de63,g4e1a3235cc+0f1912dca3,g6249c6f860+3c3976f90c,g80478fca09+46aba80bd6,g82479be7b0+77990446f6,g858d7b2824+78ba4d1ce1,g89c8672015+f667a5183b,g9125e01d80+a8ce1bb630,ga5288a1d22+2a6264e9ca,gae0086650b+a8ce1bb630,gb58c049af0+d64f4d3760,gc22bb204ba+78ba4d1ce1,gc28159a63d+638fca75ac,gcf0d15dbbd+32ddb6096f,gd6b7c0dfd1+3e339405e9,gda3e153d99+78ba4d1ce1,gda6a2b7d83+32ddb6096f,gdaeeff99f8+1711a396fd,gdd5a9049c5+b18c39e5e3,ge2409df99d+a5e4577cdc,ge33fd446bb+78ba4d1ce1,ge79ae78c31+638fca75ac,gf0baf85859+64e8883e75,gf5289d68f6+e1b046a8d7,gfa443fc69c+91d9ed1ecf,gfda6b12a05+8419469a56
LSST Data Management Base Package
Loading...
Searching...
No Matches
detectAndMeasure.py
Go to the documentation of this file.
1# This file is part of ip_diffim.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <https://www.gnu.org/licenses/>.
21
22import numpy as np
23
24import lsst.afw.detection as afwDetection
25import lsst.afw.table as afwTable
26import lsst.daf.base as dafBase
27import lsst.geom
28from lsst.ip.diffim.utils import evaluateMaskFraction
29from lsst.meas.algorithms import SkyObjectsTask, SourceDetectionTask, SetPrimaryFlagsTask, MaskStreaksTask
30from lsst.meas.base import ForcedMeasurementTask, ApplyApCorrTask, DetectorVisitIdGeneratorConfig
33import lsst.meas.extensions.shapeHSM
34import lsst.pex.config as pexConfig
35from lsst.pex.exceptions import InvalidParameterError
36import lsst.pipe.base as pipeBase
37import lsst.utils
38from lsst.utils.timer import timeMethod
39
40from . import DipoleFitTask
41
42__all__ = ["DetectAndMeasureConfig", "DetectAndMeasureTask",
43 "DetectAndMeasureScoreConfig", "DetectAndMeasureScoreTask"]
44
45
46class DetectAndMeasureConnections(pipeBase.PipelineTaskConnections,
47 dimensions=("instrument", "visit", "detector"),
48 defaultTemplates={"coaddName": "deep",
49 "warpTypeSuffix": "",
50 "fakesType": ""}):
51 science = pipeBase.connectionTypes.Input(
52 doc="Input science exposure.",
53 dimensions=("instrument", "visit", "detector"),
54 storageClass="ExposureF",
55 name="{fakesType}calexp"
56 )
57 matchedTemplate = pipeBase.connectionTypes.Input(
58 doc="Warped and PSF-matched template used to create the difference image.",
59 dimensions=("instrument", "visit", "detector"),
60 storageClass="ExposureF",
61 name="{fakesType}{coaddName}Diff_matchedExp",
62 )
63 difference = pipeBase.connectionTypes.Input(
64 doc="Result of subtracting template from science.",
65 dimensions=("instrument", "visit", "detector"),
66 storageClass="ExposureF",
67 name="{fakesType}{coaddName}Diff_differenceTempExp",
68 )
69 outputSchema = pipeBase.connectionTypes.InitOutput(
70 doc="Schema (as an example catalog) for output DIASource catalog.",
71 storageClass="SourceCatalog",
72 name="{fakesType}{coaddName}Diff_diaSrc_schema",
73 )
74 diaSources = pipeBase.connectionTypes.Output(
75 doc="Detected diaSources on the difference image.",
76 dimensions=("instrument", "visit", "detector"),
77 storageClass="SourceCatalog",
78 name="{fakesType}{coaddName}Diff_diaSrc",
79 )
80 subtractedMeasuredExposure = pipeBase.connectionTypes.Output(
81 doc="Difference image with detection mask plane filled in.",
82 dimensions=("instrument", "visit", "detector"),
83 storageClass="ExposureF",
84 name="{fakesType}{coaddName}Diff_differenceExp",
85 )
86 maskedStreaks = pipeBase.connectionTypes.Output(
87 doc='Catalog of streak fit parameters for the difference image.',
88 storageClass="ArrowNumpyDict",
89 dimensions=("instrument", "visit", "detector"),
90 name="{fakesType}{coaddName}Diff_streaks",
91 )
92
93 def __init__(self, *, config):
94 super().__init__(config=config)
95 if not (self.config.writeStreakInfo and self.config.doMaskStreaks):
96 self.outputs.remove("maskedStreaks")
97
98
99class DetectAndMeasureConfig(pipeBase.PipelineTaskConfig,
100 pipelineConnections=DetectAndMeasureConnections):
101 """Config for DetectAndMeasureTask
102 """
103 doMerge = pexConfig.Field(
104 dtype=bool,
105 default=True,
106 doc="Merge positive and negative diaSources with grow radius "
107 "set by growFootprint"
108 )
109 doForcedMeasurement = pexConfig.Field(
110 dtype=bool,
111 default=True,
112 doc="Force photometer diaSource locations on PVI?")
113 doAddMetrics = pexConfig.Field(
114 dtype=bool,
115 default=False,
116 doc="Add columns to the source table to hold analysis metrics?"
117 )
118 detection = pexConfig.ConfigurableField(
119 target=SourceDetectionTask,
120 doc="Final source detection for diaSource measurement",
121 )
122 deblend = pexConfig.ConfigurableField(
124 doc="Task to split blended sources into their components."
125 )
126 measurement = pexConfig.ConfigurableField(
127 target=DipoleFitTask,
128 doc="Task to measure sources on the difference image.",
129 )
130 doApCorr = lsst.pex.config.Field(
131 dtype=bool,
132 default=True,
133 doc="Run subtask to apply aperture corrections"
134 )
136 target=ApplyApCorrTask,
137 doc="Task to apply aperture corrections"
138 )
139 forcedMeasurement = pexConfig.ConfigurableField(
140 target=ForcedMeasurementTask,
141 doc="Task to force photometer science image at diaSource locations.",
142 )
143 growFootprint = pexConfig.Field(
144 dtype=int,
145 default=2,
146 doc="Grow positive and negative footprints by this many pixels before merging"
147 )
148 diaSourceMatchRadius = pexConfig.Field(
149 dtype=float,
150 default=0.5,
151 doc="Match radius (in arcseconds) for DiaSource to Source association"
152 )
153 doSkySources = pexConfig.Field(
154 dtype=bool,
155 default=False,
156 doc="Generate sky sources?",
157 )
158 skySources = pexConfig.ConfigurableField(
159 target=SkyObjectsTask,
160 doc="Generate sky sources",
161 )
162 doMaskStreaks = pexConfig.Field(
163 dtype=bool,
164 default=True,
165 doc="Turn on streak masking",
166 )
167 maskStreaks = pexConfig.ConfigurableField(
168 target=MaskStreaksTask,
169 doc="Subtask for masking streaks. Only used if doMaskStreaks is True. "
170 "Adds a mask plane to an exposure, with the mask plane name set by streakMaskName.",
171 )
172 writeStreakInfo = pexConfig.Field(
173 dtype=bool,
174 default=False,
175 doc="Record the parameters of any detected streaks. For LSST, this should be turned off except for "
176 "development work."
177 )
178 setPrimaryFlags = pexConfig.ConfigurableField(
179 target=SetPrimaryFlagsTask,
180 doc="Task to add isPrimary and deblending-related flags to the catalog."
181 )
182 badSourceFlags = lsst.pex.config.ListField(
183 dtype=str,
184 doc="Sources with any of these flags set are removed before writing the output catalog.",
185 default=("base_PixelFlags_flag_offimage",
186 "base_PixelFlags_flag_interpolatedCenterAll",
187 "base_PixelFlags_flag_badCenterAll",
188 "base_PixelFlags_flag_edgeCenterAll",
189 "base_PixelFlags_flag_saturatedCenterAll",
190 ),
191 )
192 idGenerator = DetectorVisitIdGeneratorConfig.make_field()
193
194 def setDefaults(self):
195 # DiaSource Detection
196 self.detection.thresholdPolarity = "both"
197 self.detection.thresholdValue = 5.0
198 self.detection.reEstimateBackground = False
199 self.detection.thresholdType = "pixel_stdev"
200 self.detection.excludeMaskPlanes = ["EDGE", "SAT", "BAD", "INTRP"]
201
202 self.measurement.plugins.names |= ["ext_trailedSources_Naive",
203 "base_LocalPhotoCalib",
204 "base_LocalWcs",
205 "ext_shapeHSM_HsmSourceMoments",
206 "ext_shapeHSM_HsmPsfMoments",
207 ]
208 self.measurement.slots.psfShape = "ext_shapeHSM_HsmPsfMoments"
209 self.measurement.slots.shape = "ext_shapeHSM_HsmSourceMoments"
210 self.measurement.plugins["base_SdssCentroid"].maxDistToPeak = 5.0
211 self.forcedMeasurement.plugins = ["base_TransformedCentroid", "base_PsfFlux"]
212 self.forcedMeasurement.copyColumns = {
213 "id": "objectId", "parent": "parentObjectId", "coord_ra": "coord_ra", "coord_dec": "coord_dec"}
214 self.forcedMeasurement.slots.centroid = "base_TransformedCentroid"
215 self.forcedMeasurement.slots.shape = None
216
217 # Keep track of which footprints contain streaks
218 self.measurement.plugins["base_PixelFlags"].masksFpAnywhere = [
219 "STREAK", "INJECTED", "INJECTED_TEMPLATE"]
220 self.measurement.plugins["base_PixelFlags"].masksFpCenter = [
221 "STREAK", "INJECTED", "INJECTED_TEMPLATE"]
222 self.skySources.avoidMask = ["DETECTED", "DETECTED_NEGATIVE", "BAD", "NO_DATA", "EDGE"]
223
224 # Set the streak mask along the entire fit line, not only where the
225 # detected mask is set.
226 self.maskStreaks.onlyMaskDetected = False
227
228
229class DetectAndMeasureTask(lsst.pipe.base.PipelineTask):
230 """Detect and measure sources on a difference image.
231 """
232 ConfigClass = DetectAndMeasureConfig
233 _DefaultName = "detectAndMeasure"
234
235 def __init__(self, **kwargs):
236 super().__init__(**kwargs)
237 self.schema = afwTable.SourceTable.makeMinimalSchema()
238 # Add coordinate error fields:
239 afwTable.CoordKey.addErrorFields(self.schema)
240
241 self.algMetadata = dafBase.PropertyList()
242 self.makeSubtask("detection", schema=self.schema)
243 self.makeSubtask("deblend", schema=self.schema)
244 self.makeSubtask("setPrimaryFlags", schema=self.schema, isSingleFrame=True)
245 self.makeSubtask("measurement", schema=self.schema,
246 algMetadata=self.algMetadata)
247 if self.config.doApCorr:
248 self.makeSubtask("applyApCorr", schema=self.measurement.schema)
249 if self.config.doForcedMeasurement:
250 self.schema.addField(
251 "ip_diffim_forced_PsfFlux_instFlux", "D",
252 "Forced PSF flux measured on the direct image.",
253 units="count")
254 self.schema.addField(
255 "ip_diffim_forced_PsfFlux_instFluxErr", "D",
256 "Forced PSF flux error measured on the direct image.",
257 units="count")
258 self.schema.addField(
259 "ip_diffim_forced_PsfFlux_area", "F",
260 "Forced PSF flux effective area of PSF.",
261 units="pixel")
262 self.schema.addField(
263 "ip_diffim_forced_PsfFlux_flag", "Flag",
264 "Forced PSF flux general failure flag.")
265 self.schema.addField(
266 "ip_diffim_forced_PsfFlux_flag_noGoodPixels", "Flag",
267 "Forced PSF flux not enough non-rejected pixels in data to attempt the fit.")
268 self.schema.addField(
269 "ip_diffim_forced_PsfFlux_flag_edge", "Flag",
270 "Forced PSF flux object was too close to the edge of the image to use the full PSF model.")
271 self.makeSubtask("forcedMeasurement", refSchema=self.schema)
272
273 self.schema.addField("refMatchId", "L", "unique id of reference catalog match")
274 self.schema.addField("srcMatchId", "L", "unique id of source match")
275 if self.config.doSkySources:
276 self.makeSubtask("skySources", schema=self.schema)
277 if self.config.doMaskStreaks:
278 self.makeSubtask("maskStreaks")
279
280 # Check that the schema and config are consistent
281 for flag in self.config.badSourceFlags:
282 if flag not in self.schema:
283 raise pipeBase.InvalidQuantumError("Field %s not in schema" % flag)
284
285 # initialize InitOutputs
286 self.outputSchema = afwTable.SourceCatalog(self.schema)
287 self.outputSchema.getTable().setMetadata(self.algMetadata)
288
289 def runQuantum(self, butlerQC: pipeBase.QuantumContext,
290 inputRefs: pipeBase.InputQuantizedConnection,
291 outputRefs: pipeBase.OutputQuantizedConnection):
292 inputs = butlerQC.get(inputRefs)
293 idGenerator = self.config.idGenerator.apply(butlerQC.quantum.dataId)
294 idFactory = idGenerator.make_table_id_factory()
295 outputs = self.run(**inputs, idFactory=idFactory)
296 butlerQC.put(outputs, outputRefs)
297
298 @timeMethod
299 def run(self, science, matchedTemplate, difference,
300 idFactory=None):
301 """Detect and measure sources on a difference image.
302
303 The difference image will be convolved with a gaussian approximation of
304 the PSF to form a maximum likelihood image for detection.
305 Close positive and negative detections will optionally be merged into
306 dipole diaSources.
307 Sky sources, or forced detections in background regions, will optionally
308 be added, and the configured measurement algorithm will be run on all
309 detections.
310
311 Parameters
312 ----------
313 science : `lsst.afw.image.ExposureF`
314 Science exposure that the template was subtracted from.
315 matchedTemplate : `lsst.afw.image.ExposureF`
316 Warped and PSF-matched template that was used produce the
317 difference image.
318 difference : `lsst.afw.image.ExposureF`
319 Result of subtracting template from the science image.
320 idFactory : `lsst.afw.table.IdFactory`, optional
321 Generator object used to assign ids to detected sources in the
322 difference image. Ids from this generator are not set until after
323 deblending and merging positive/negative peaks.
324
325 Returns
326 -------
327 measurementResults : `lsst.pipe.base.Struct`
328
329 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
330 Subtracted exposure with detection mask applied.
331 ``diaSources`` : `lsst.afw.table.SourceCatalog`
332 The catalog of detected sources.
333 """
334 if idFactory is None:
335 idFactory = lsst.meas.base.IdGenerator().make_table_id_factory()
336
337 # Ensure that we start with an empty detection and deblended mask.
338 mask = difference.mask
339 clearMaskPlanes = ["DETECTED", "DETECTED_NEGATIVE", "NOT_DEBLENDED", "STREAK"]
340 for mp in clearMaskPlanes:
341 if mp not in mask.getMaskPlaneDict():
342 mask.addMaskPlane(mp)
343 mask &= ~mask.getPlaneBitMask(clearMaskPlanes)
344
345 # Don't use the idFactory until after deblend+merge, so that we aren't
346 # generating ids that just get thrown away (footprint merge doesn't
347 # know about past ids).
348 table = afwTable.SourceTable.make(self.schema)
349 results = self.detection.run(
350 table=table,
351 exposure=difference,
352 doSmooth=True,
353 )
354
355 sources, positives, negatives = self._deblend(difference,
356 results.positive,
357 results.negative)
358
359 return self.processResults(science, matchedTemplate, difference, sources, idFactory,
360 positiveFootprints=positives,
361 negativeFootprints=negatives)
362
363 def processResults(self, science, matchedTemplate, difference, sources, idFactory,
364 positiveFootprints=None, negativeFootprints=None,):
365 """Measure and process the results of source detection.
366
367 Parameters
368 ----------
369 science : `lsst.afw.image.ExposureF`
370 Science exposure that the template was subtracted from.
371 matchedTemplate : `lsst.afw.image.ExposureF`
372 Warped and PSF-matched template that was used produce the
373 difference image.
374 difference : `lsst.afw.image.ExposureF`
375 Result of subtracting template from the science image.
376 sources : `lsst.afw.table.SourceCatalog`
377 Detected sources on the difference exposure.
378 idFactory : `lsst.afw.table.IdFactory`
379 Generator object used to assign ids to detected sources in the
380 difference image.
381 positiveFootprints : `lsst.afw.detection.FootprintSet`, optional
382 Positive polarity footprints.
383 negativeFootprints : `lsst.afw.detection.FootprintSet`, optional
384 Negative polarity footprints.
385
386 Returns
387 -------
388 measurementResults : `lsst.pipe.base.Struct`
389
390 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
391 Subtracted exposure with detection mask applied.
392 ``diaSources`` : `lsst.afw.table.SourceCatalog`
393 The catalog of detected sources.
394 """
395 self.metadata.add("nUnmergedDiaSources", len(sources))
396 if self.config.doMerge:
397 fpSet = positiveFootprints
398 fpSet.merge(negativeFootprints, self.config.growFootprint,
399 self.config.growFootprint, False)
400 initialDiaSources = afwTable.SourceCatalog(self.schema)
401 fpSet.makeSources(initialDiaSources)
402 self.log.info("Merging detections into %d sources", len(initialDiaSources))
403 else:
404 initialDiaSources = sources
405
406 # Assign source ids at the end: deblend/merge mean that we don't keep
407 # track of parents and children, we only care about the final ids.
408 for source in initialDiaSources:
409 source.setId(idFactory())
410 # Ensure sources added after this get correct ids.
411 initialDiaSources.getTable().setIdFactory(idFactory)
412 initialDiaSources.setMetadata(self.algMetadata)
413
414 self.metadata.add("nMergedDiaSources", len(initialDiaSources))
415
416 if self.config.doMaskStreaks:
417 streakInfo = self._runStreakMasking(difference.maskedImage)
418
419 if self.config.doSkySources:
420 self.addSkySources(initialDiaSources, difference.mask, difference.info.id)
421
422 if not initialDiaSources.isContiguous():
423 initialDiaSources = initialDiaSources.copy(deep=True)
424
425 self.measureDiaSources(initialDiaSources, science, difference, matchedTemplate)
426 diaSources = self._removeBadSources(initialDiaSources)
427
428 if self.config.doForcedMeasurement:
429 self.measureForcedSources(diaSources, science, difference.getWcs())
430
431 self.calculateMetrics(difference)
432
433 measurementResults = pipeBase.Struct(
434 subtractedMeasuredExposure=difference,
435 diaSources=diaSources,
436 )
437 if self.config.doMaskStreaks and self.config.writeStreakInfo:
438 measurementResults.mergeItems(streakInfo, 'maskedStreaks')
439
440 return measurementResults
441
442 def _deblend(self, difference, positiveFootprints, negativeFootprints):
443 """Deblend the positive and negative footprints and return a catalog
444 containing just the children, and the deblended footprints.
445
446 Parameters
447 ----------
448 difference : `lsst.afw.image.Exposure`
449 Result of subtracting template from the science image.
450 positiveFootprints, negativeFootprints : `lsst.afw.detection.FootprintSet`
451 Positive and negative polarity footprints measured on
452 ``difference`` to be deblended separately.
453
454 Returns
455 -------
456 sources : `lsst.afw.table.SourceCatalog`
457 Positive and negative deblended children.
458 positives, negatives : `lsst.afw.detection.FootprintSet`
459 Deblended positive and negative polarity footprints measured on
460 ``difference``.
461 """
462 def makeFootprints(sources):
463 footprints = afwDetection.FootprintSet(difference.getBBox())
464 footprints.setFootprints([src.getFootprint() for src in sources])
465 return footprints
466
467 def deblend(footprints):
468 """Deblend a positive or negative footprint set,
469 and return the deblended children.
470 """
471 sources = afwTable.SourceCatalog(self.schema)
472 footprints.makeSources(sources)
473 self.deblend.run(exposure=difference, sources=sources)
474 self.setPrimaryFlags.run(sources)
475 children = sources["detect_isDeblendedSource"] == 1
476 sources = sources[children].copy(deep=True)
477 # Clear parents, so that measurement plugins behave correctly.
478 sources['parent'] = 0
479 return sources.copy(deep=True)
480
481 positives = deblend(positiveFootprints)
482 negatives = deblend(negativeFootprints)
483
484 sources = afwTable.SourceCatalog(self.schema)
485 sources.reserve(len(positives) + len(negatives))
486 sources.extend(positives, deep=True)
487 sources.extend(negatives, deep=True)
488 return sources, makeFootprints(positives), makeFootprints(negatives)
489
490 def _removeBadSources(self, diaSources):
491 """Remove unphysical diaSources from the catalog.
492
493 Parameters
494 ----------
495 diaSources : `lsst.afw.table.SourceCatalog`
496 The catalog of detected sources.
497
498 Returns
499 -------
500 diaSources : `lsst.afw.table.SourceCatalog`
501 The updated catalog of detected sources, with any source that has a
502 flag in ``config.badSourceFlags`` set removed.
503 """
504 selector = np.ones(len(diaSources), dtype=bool)
505 for flag in self.config.badSourceFlags:
506 flags = diaSources[flag]
507 nBad = np.count_nonzero(flags)
508 if nBad > 0:
509 self.log.debug("Found %d unphysical sources with flag %s.", nBad, flag)
510 selector &= ~flags
511 nBadTotal = np.count_nonzero(~selector)
512 self.metadata.add("nRemovedBadFlaggedSources", nBadTotal)
513 self.log.info("Removed %d unphysical sources.", nBadTotal)
514 return diaSources[selector].copy(deep=True)
515
516 def addSkySources(self, diaSources, mask, seed,
517 subtask=None):
518 """Add sources in empty regions of the difference image
519 for measuring the background.
520
521 Parameters
522 ----------
523 diaSources : `lsst.afw.table.SourceCatalog`
524 The catalog of detected sources.
525 mask : `lsst.afw.image.Mask`
526 Mask plane for determining regions where Sky sources can be added.
527 seed : `int`
528 Seed value to initialize the random number generator.
529 """
530 if subtask is None:
531 subtask = self.skySources
532 skySourceFootprints = subtask.run(mask=mask, seed=seed, catalog=diaSources)
533 self.metadata.add(f"n_{subtask.getName()}", len(skySourceFootprints))
534
535 def measureDiaSources(self, diaSources, science, difference, matchedTemplate):
536 """Use (matched) template and science image to constrain dipole fitting.
537
538 Parameters
539 ----------
540 diaSources : `lsst.afw.table.SourceCatalog`
541 The catalog of detected sources.
542 science : `lsst.afw.image.ExposureF`
543 Science exposure that the template was subtracted from.
544 difference : `lsst.afw.image.ExposureF`
545 Result of subtracting template from the science image.
546 matchedTemplate : `lsst.afw.image.ExposureF`
547 Warped and PSF-matched template that was used produce the
548 difference image.
549 """
550 # Ensure that the required mask planes are present
551 for mp in self.config.measurement.plugins["base_PixelFlags"].masksFpAnywhere:
552 difference.mask.addMaskPlane(mp)
553 # Note that this may not be correct if we convolved the science image.
554 # In the future we may wish to persist the matchedScience image.
555 self.measurement.run(diaSources, difference, science, matchedTemplate)
556 if self.config.doApCorr:
557 apCorrMap = difference.getInfo().getApCorrMap()
558 if apCorrMap is None:
559 self.log.warning("Difference image does not have valid aperture correction; skipping.")
560 else:
561 self.applyApCorr.run(
562 catalog=diaSources,
563 apCorrMap=apCorrMap,
564 )
565
566 def measureForcedSources(self, diaSources, science, wcs):
567 """Perform forced measurement of the diaSources on the science image.
568
569 Parameters
570 ----------
571 diaSources : `lsst.afw.table.SourceCatalog`
572 The catalog of detected sources.
573 science : `lsst.afw.image.ExposureF`
574 Science exposure that the template was subtracted from.
575 wcs : `lsst.afw.geom.SkyWcs`
576 Coordinate system definition (wcs) for the exposure.
577 """
578 # Run forced psf photometry on the PVI at the diaSource locations.
579 # Copy the measured flux and error into the diaSource.
580 forcedSources = self.forcedMeasurement.generateMeasCat(science, diaSources, wcs)
581 self.forcedMeasurement.run(forcedSources, science, diaSources, wcs)
582 mapper = afwTable.SchemaMapper(forcedSources.schema, diaSources.schema)
583 mapper.addMapping(forcedSources.schema.find("base_PsfFlux_instFlux")[0],
584 "ip_diffim_forced_PsfFlux_instFlux", True)
585 mapper.addMapping(forcedSources.schema.find("base_PsfFlux_instFluxErr")[0],
586 "ip_diffim_forced_PsfFlux_instFluxErr", True)
587 mapper.addMapping(forcedSources.schema.find("base_PsfFlux_area")[0],
588 "ip_diffim_forced_PsfFlux_area", True)
589 mapper.addMapping(forcedSources.schema.find("base_PsfFlux_flag")[0],
590 "ip_diffim_forced_PsfFlux_flag", True)
591 mapper.addMapping(forcedSources.schema.find("base_PsfFlux_flag_noGoodPixels")[0],
592 "ip_diffim_forced_PsfFlux_flag_noGoodPixels", True)
593 mapper.addMapping(forcedSources.schema.find("base_PsfFlux_flag_edge")[0],
594 "ip_diffim_forced_PsfFlux_flag_edge", True)
595 for diaSource, forcedSource in zip(diaSources, forcedSources):
596 diaSource.assign(forcedSource, mapper)
597
598 def calculateMetrics(self, difference):
599 """Add difference image QA metrics to the Task metadata.
600
601 This may be used to produce corresponding metrics (see
602 lsst.analysis.tools.tasks.diffimTaskDetectorVisitMetricAnalysis).
603
604 Parameters
605 ----------
606 difference : `lsst.afw.image.Exposure`
607 The target difference image to calculate metrics for.
608 """
609 mask = difference.mask
610 badPix = (mask.array & mask.getPlaneBitMask(self.config.detection.excludeMaskPlanes)) > 0
611 self.metadata.add("nGoodPixels", np.sum(~badPix))
612 self.metadata.add("nBadPixels", np.sum(badPix))
613 detPosPix = (mask.array & mask.getPlaneBitMask("DETECTED")) > 0
614 detNegPix = (mask.array & mask.getPlaneBitMask("DETECTED_NEGATIVE")) > 0
615 self.metadata.add("nPixelsDetectedPositive", np.sum(detPosPix))
616 self.metadata.add("nPixelsDetectedNegative", np.sum(detNegPix))
617 detPosPix &= badPix
618 detNegPix &= badPix
619 self.metadata.add("nBadPixelsDetectedPositive", np.sum(detPosPix))
620 self.metadata.add("nBadPixelsDetectedNegative", np.sum(detNegPix))
621
622 metricsMaskPlanes = list(mask.getMaskPlaneDict().keys())
623 for maskPlane in metricsMaskPlanes:
624 try:
625 self.metadata.add("%s_mask_fraction"%maskPlane.lower(), evaluateMaskFraction(mask, maskPlane))
626 except InvalidParameterError:
627 self.metadata.add("%s_mask_fraction"%maskPlane.lower(), -1)
628 self.log.info("Unable to calculate metrics for mask plane %s: not in image"%maskPlane)
629
630 def _runStreakMasking(self, maskedImage):
631 """Do streak masking and optionally save the resulting streak
632 fit parameters in a catalog.
633
634 Parameters
635 ----------
636 maskedImage: `lsst.afw.image.maskedImage`
637 The image in which to search for streaks. Must have a detection
638 mask.
639
640 Returns
641 -------
642 streakInfo: `lsst.pipe.base.Struct`
643 ``rho`` : `np.ndarray`
644 Angle of detected streak.
645 ``theta`` : `np.ndarray`
646 Distance from center of detected streak.
647 ``sigma`` : `np.ndarray`
648 Width of streak profile.
649 ``reducedChi2`` : `np.ndarray`
650 Reduced chi2 of the best-fit streak profile.
651 ``modelMaximum`` : `np.ndarray`
652 Peak value of the fit line profile.
653 """
654 streaks = self.maskStreaks.run(maskedImage)
655 if self.config.writeStreakInfo:
656 rhos = np.array([line.rho for line in streaks.lines])
657 thetas = np.array([line.theta for line in streaks.lines])
658 sigmas = np.array([line.sigma for line in streaks.lines])
659 chi2s = np.array([line.reducedChi2 for line in streaks.lines])
660 modelMaximums = np.array([line.modelMaximum for line in streaks.lines])
661 streakInfo = {'rho': rhos, 'theta': thetas, 'sigma': sigmas, 'reducedChi2': chi2s,
662 'modelMaximum': modelMaximums}
663 else:
664 streakInfo = {'rho': np.array([]), 'theta': np.array([]), 'sigma': np.array([]),
665 'reducedChi2': np.array([]), 'modelMaximum': np.array([])}
666 return pipeBase.Struct(maskedStreaks=streakInfo)
667
668
669class DetectAndMeasureScoreConnections(DetectAndMeasureConnections):
670 scoreExposure = pipeBase.connectionTypes.Input(
671 doc="Maximum likelihood image for detection.",
672 dimensions=("instrument", "visit", "detector"),
673 storageClass="ExposureF",
674 name="{fakesType}{coaddName}Diff_scoreExp",
675 )
676
677
678class DetectAndMeasureScoreConfig(DetectAndMeasureConfig,
679 pipelineConnections=DetectAndMeasureScoreConnections):
680 pass
681
682
683class DetectAndMeasureScoreTask(DetectAndMeasureTask):
684 """Detect DIA sources using a score image,
685 and measure the detections on the difference image.
686
687 Source detection is run on the supplied score, or maximum likelihood,
688 image. Note that no additional convolution will be done in this case.
689 Close positive and negative detections will optionally be merged into
690 dipole diaSources.
691 Sky sources, or forced detections in background regions, will optionally
692 be added, and the configured measurement algorithm will be run on all
693 detections.
694 """
695 ConfigClass = DetectAndMeasureScoreConfig
696 _DefaultName = "detectAndMeasureScore"
697
698 @timeMethod
699 def run(self, science, matchedTemplate, difference, scoreExposure,
700 idFactory=None):
701 """Detect and measure sources on a score image.
702
703 Parameters
704 ----------
705 science : `lsst.afw.image.ExposureF`
706 Science exposure that the template was subtracted from.
707 matchedTemplate : `lsst.afw.image.ExposureF`
708 Warped and PSF-matched template that was used produce the
709 difference image.
710 difference : `lsst.afw.image.ExposureF`
711 Result of subtracting template from the science image.
712 scoreExposure : `lsst.afw.image.ExposureF`
713 Score or maximum likelihood difference image
714 idFactory : `lsst.afw.table.IdFactory`, optional
715 Generator object used to assign ids to detected sources in the
716 difference image. Ids from this generator are not set until after
717 deblending and merging positive/negative peaks.
718
719 Returns
720 -------
721 measurementResults : `lsst.pipe.base.Struct`
722
723 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
724 Subtracted exposure with detection mask applied.
725 ``diaSources`` : `lsst.afw.table.SourceCatalog`
726 The catalog of detected sources.
727 """
728 if idFactory is None:
729 idFactory = lsst.meas.base.IdGenerator().make_table_id_factory()
730
731 # Ensure that we start with an empty detection mask.
732 mask = scoreExposure.mask
733 mask &= ~(mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE"))
734
735 # Don't use the idFactory until after deblend+merge, so that we aren't
736 # generating ids that just get thrown away (footprint merge doesn't
737 # know about past ids).
738 table = afwTable.SourceTable.make(self.schema)
739 results = self.detection.run(
740 table=table,
741 exposure=scoreExposure,
742 doSmooth=False,
743 )
744 # Copy the detection mask from the Score image to the difference image
745 difference.mask.assign(scoreExposure.mask, scoreExposure.getBBox())
746
747 sources, positives, negatives = self._deblend(difference,
748 results.positive,
749 results.negative)
750
751 return self.processResults(science, matchedTemplate, difference, sources, idFactory,
752 positiveFootprints=positives, negativeFootprints=negatives)
A mapping between the keys of two Schemas, used to copy data between them.
Class for storing ordered metadata with comments.
run(self, coaddExposures, bbox, wcs, dataIds, physical_filter)