LSST Applications g063fba187b+cac8b7c890,g0f08755f38+6aee506743,g1653933729+a8ce1bb630,g168dd56ebc+a8ce1bb630,g1a2382251a+b4475c5878,g1dcb35cd9c+8f9bc1652e,g20f6ffc8e0+6aee506743,g217e2c1bcf+73dee94bd0,g28da252d5a+1f19c529b9,g2bbee38e9b+3f2625acfc,g2bc492864f+3f2625acfc,g3156d2b45e+6e55a43351,g32e5bea42b+1bb94961c2,g347aa1857d+3f2625acfc,g35bb328faa+a8ce1bb630,g3a166c0a6a+3f2625acfc,g3e281a1b8c+c5dd892a6c,g3e8969e208+a8ce1bb630,g414038480c+5927e1bc1e,g41af890bb2+8a9e676b2a,g7af13505b9+809c143d88,g80478fca09+6ef8b1810f,g82479be7b0+f568feb641,g858d7b2824+6aee506743,g89c8672015+f4add4ffd5,g9125e01d80+a8ce1bb630,ga5288a1d22+2903d499ea,gb58c049af0+d64f4d3760,gc28159a63d+3f2625acfc,gcab2d0539d+b12535109e,gcf0d15dbbd+46a3f46ba9,gda6a2b7d83+46a3f46ba9,gdaeeff99f8+1711a396fd,ge79ae78c31+3f2625acfc,gef2f8181fd+0a71e47438,gf0baf85859+c1f95f4921,gfa517265be+6aee506743,gfa999e8aa5+17cd334064,w.2024.51
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.image as afwImage
26import lsst.afw.math as afwMath
27import lsst.afw.table as afwTable
28import lsst.daf.base as dafBase
29import lsst.geom
30from lsst.ip.diffim.utils import evaluateMaskFraction
31from lsst.meas.algorithms import SkyObjectsTask, SourceDetectionTask, SetPrimaryFlagsTask, MaskStreaksTask
32from lsst.meas.base import ForcedMeasurementTask, ApplyApCorrTask, DetectorVisitIdGeneratorConfig
35import lsst.meas.extensions.shapeHSM
36import lsst.pex.config as pexConfig
37from lsst.pex.exceptions import InvalidParameterError
38import lsst.pipe.base as pipeBase
39import lsst.utils
40from lsst.utils.timer import timeMethod
41
42from . import DipoleFitTask
43
44__all__ = ["DetectAndMeasureConfig", "DetectAndMeasureTask",
45 "DetectAndMeasureScoreConfig", "DetectAndMeasureScoreTask"]
46
47
48class DetectAndMeasureConnections(pipeBase.PipelineTaskConnections,
49 dimensions=("instrument", "visit", "detector"),
50 defaultTemplates={"coaddName": "deep",
51 "warpTypeSuffix": "",
52 "fakesType": ""}):
53 science = pipeBase.connectionTypes.Input(
54 doc="Input science exposure.",
55 dimensions=("instrument", "visit", "detector"),
56 storageClass="ExposureF",
57 name="{fakesType}calexp"
58 )
59 matchedTemplate = pipeBase.connectionTypes.Input(
60 doc="Warped and PSF-matched template used to create the difference image.",
61 dimensions=("instrument", "visit", "detector"),
62 storageClass="ExposureF",
63 name="{fakesType}{coaddName}Diff_matchedExp",
64 )
65 difference = pipeBase.connectionTypes.Input(
66 doc="Result of subtracting template from science.",
67 dimensions=("instrument", "visit", "detector"),
68 storageClass="ExposureF",
69 name="{fakesType}{coaddName}Diff_differenceTempExp",
70 )
71 outputSchema = pipeBase.connectionTypes.InitOutput(
72 doc="Schema (as an example catalog) for output DIASource catalog.",
73 storageClass="SourceCatalog",
74 name="{fakesType}{coaddName}Diff_diaSrc_schema",
75 )
76 diaSources = pipeBase.connectionTypes.Output(
77 doc="Detected diaSources on the difference image.",
78 dimensions=("instrument", "visit", "detector"),
79 storageClass="SourceCatalog",
80 name="{fakesType}{coaddName}Diff_diaSrc",
81 )
82 subtractedMeasuredExposure = pipeBase.connectionTypes.Output(
83 doc="Difference image with detection mask plane filled in.",
84 dimensions=("instrument", "visit", "detector"),
85 storageClass="ExposureF",
86 name="{fakesType}{coaddName}Diff_differenceExp",
87 )
88 maskedStreaks = pipeBase.connectionTypes.Output(
89 doc='Catalog of streak fit parameters for the difference image.',
90 storageClass="ArrowNumpyDict",
91 dimensions=("instrument", "visit", "detector"),
92 name="{fakesType}{coaddName}Diff_streaks",
93 )
94
95 def __init__(self, *, config):
96 super().__init__(config=config)
97 if not (self.config.writeStreakInfo and self.config.doMaskStreaks):
98 self.outputs.remove("maskedStreaks")
99
100
101class DetectAndMeasureConfig(pipeBase.PipelineTaskConfig,
102 pipelineConnections=DetectAndMeasureConnections):
103 """Config for DetectAndMeasureTask
104 """
105 doMerge = pexConfig.Field(
106 dtype=bool,
107 default=True,
108 doc="Merge positive and negative diaSources with grow radius "
109 "set by growFootprint"
110 )
111 doForcedMeasurement = pexConfig.Field(
112 dtype=bool,
113 default=True,
114 doc="Force photometer diaSource locations on PVI?")
115 doAddMetrics = pexConfig.Field(
116 dtype=bool,
117 default=False,
118 doc="Add columns to the source table to hold analysis metrics?"
119 )
120 detection = pexConfig.ConfigurableField(
121 target=SourceDetectionTask,
122 doc="Final source detection for diaSource measurement",
123 )
124 streakDetection = pexConfig.ConfigurableField(
125 target=SourceDetectionTask,
126 doc="Separate source detection used only for streak masking",
127 )
128 deblend = pexConfig.ConfigurableField(
130 doc="Task to split blended sources into their components."
131 )
132 measurement = pexConfig.ConfigurableField(
133 target=DipoleFitTask,
134 doc="Task to measure sources on the difference image.",
135 )
136 doApCorr = lsst.pex.config.Field(
137 dtype=bool,
138 default=True,
139 doc="Run subtask to apply aperture corrections"
140 )
142 target=ApplyApCorrTask,
143 doc="Task to apply aperture corrections"
144 )
145 forcedMeasurement = pexConfig.ConfigurableField(
146 target=ForcedMeasurementTask,
147 doc="Task to force photometer science image at diaSource locations.",
148 )
149 growFootprint = pexConfig.Field(
150 dtype=int,
151 default=2,
152 doc="Grow positive and negative footprints by this many pixels before merging"
153 )
154 diaSourceMatchRadius = pexConfig.Field(
155 dtype=float,
156 default=0.5,
157 doc="Match radius (in arcseconds) for DiaSource to Source association"
158 )
159 doSkySources = pexConfig.Field(
160 dtype=bool,
161 default=False,
162 doc="Generate sky sources?",
163 )
164 skySources = pexConfig.ConfigurableField(
165 target=SkyObjectsTask,
166 doc="Generate sky sources",
167 )
168 doMaskStreaks = pexConfig.Field(
169 dtype=bool,
170 default=True,
171 doc="Turn on streak masking",
172 )
173 maskStreaks = pexConfig.ConfigurableField(
174 target=MaskStreaksTask,
175 doc="Subtask for masking streaks. Only used if doMaskStreaks is True. "
176 "Adds a mask plane to an exposure, with the mask plane name set by streakMaskName.",
177 )
178 streakBinFactor = pexConfig.Field(
179 dtype=int,
180 default=4,
181 doc="Bin scale factor to use when rerunning detection for masking streaks. "
182 "Only used if doMaskStreaks is True.",
183 )
184 writeStreakInfo = pexConfig.Field(
185 dtype=bool,
186 default=False,
187 doc="Record the parameters of any detected streaks. For LSST, this should be turned off except for "
188 "development work."
189 )
190 setPrimaryFlags = pexConfig.ConfigurableField(
191 target=SetPrimaryFlagsTask,
192 doc="Task to add isPrimary and deblending-related flags to the catalog."
193 )
194 badSourceFlags = lsst.pex.config.ListField(
195 dtype=str,
196 doc="Sources with any of these flags set are removed before writing the output catalog.",
197 default=("base_PixelFlags_flag_offimage",
198 "base_PixelFlags_flag_interpolatedCenterAll",
199 "base_PixelFlags_flag_badCenterAll",
200 "base_PixelFlags_flag_edgeCenterAll",
201 "base_PixelFlags_flag_saturatedCenterAll",
202 ),
203 )
204 clearMaskPlanes = lsst.pex.config.ListField(
205 dtype=str,
206 doc="Mask planes to clear before running detection.",
207 default=("DETECTED", "DETECTED_NEGATIVE", "NOT_DEBLENDED", "STREAK"),
208 )
209 idGenerator = DetectorVisitIdGeneratorConfig.make_field()
210
211 def setDefaults(self):
212 # DiaSource Detection
213 self.detection.thresholdPolarity = "both"
214 self.detection.thresholdValue = 5.0
215 self.detection.reEstimateBackground = False
216 self.detection.thresholdType = "pixel_stdev"
217 self.detection.excludeMaskPlanes = ["EDGE",
218 "SAT",
219 "BAD",
220 "INTRP",
221 "NO_DATA",
222 ]
223
224 # Copy configs for binned streak detection from the base detection task
225 self.streakDetection.thresholdType = self.detection.thresholdType
226 self.streakDetection.reEstimateBackground = self.detection.reEstimateBackground
227 self.streakDetection.excludeMaskPlanes = self.detection.excludeMaskPlanes
228 self.streakDetection.thresholdValue = self.detection.thresholdValue
229 # Only detect positive streaks
230 self.streakDetection.thresholdPolarity = "positive"
231 # Do not grow detected mask for streaks
232 self.streakDetection.nSigmaToGrow = 0
233 # Set the streak mask along the entire fit line, not only where the
234 # detected mask is set.
235 self.maskStreaks.onlyMaskDetected = False
236 # Restrict streak masking from growing too large
237 self.maskStreaks.maxStreakWidth = 100
238 # Restrict the number of iterations allowed for fitting streaks
239 # When the fit is good it should solve quickly, and exit a bad fit quickly
240 self.maskStreaks.maxFitIter = 10
241 # Only mask to 2 sigma in width
242 self.maskStreaks.nSigmaMask = 2
243 # Threshold for including streaks after the Hough Transform.
244 # A lower value will detect more features that are less linear.
245 self.maskStreaks.absMinimumKernelHeight = 2
246
247 self.measurement.plugins.names |= ["ext_trailedSources_Naive",
248 "base_LocalPhotoCalib",
249 "base_LocalWcs",
250 "ext_shapeHSM_HsmSourceMoments",
251 "ext_shapeHSM_HsmPsfMoments",
252 "base_ClassificationSizeExtendedness",
253 ]
254 self.measurement.slots.psfShape = "ext_shapeHSM_HsmPsfMoments"
255 self.measurement.slots.shape = "ext_shapeHSM_HsmSourceMoments"
256 self.measurement.plugins["base_SdssCentroid"].maxDistToPeak = 5.0
257 self.forcedMeasurement.plugins = ["base_TransformedCentroid", "base_PsfFlux"]
258 self.forcedMeasurement.copyColumns = {
259 "id": "objectId", "parent": "parentObjectId", "coord_ra": "coord_ra", "coord_dec": "coord_dec"}
260 self.forcedMeasurement.slots.centroid = "base_TransformedCentroid"
261 self.forcedMeasurement.slots.shape = None
262
263 # Keep track of which footprints contain streaks
264 self.measurement.plugins["base_PixelFlags"].masksFpAnywhere = [
265 "STREAK", "INJECTED", "INJECTED_TEMPLATE"]
266 self.measurement.plugins["base_PixelFlags"].masksFpCenter = [
267 "STREAK", "INJECTED", "INJECTED_TEMPLATE"]
268 self.skySources.avoidMask = ["DETECTED", "DETECTED_NEGATIVE", "BAD", "NO_DATA", "EDGE"]
269
270
271class DetectAndMeasureTask(lsst.pipe.base.PipelineTask):
272 """Detect and measure sources on a difference image.
273 """
274 ConfigClass = DetectAndMeasureConfig
275 _DefaultName = "detectAndMeasure"
276
277 def __init__(self, **kwargs):
278 super().__init__(**kwargs)
279 self.schema = afwTable.SourceTable.makeMinimalSchema()
280 # Add coordinate error fields:
281 afwTable.CoordKey.addErrorFields(self.schema)
282
283 self.algMetadata = dafBase.PropertyList()
284 self.makeSubtask("detection", schema=self.schema)
285 self.makeSubtask("deblend", schema=self.schema)
286 self.makeSubtask("setPrimaryFlags", schema=self.schema, isSingleFrame=True)
287 self.makeSubtask("measurement", schema=self.schema,
288 algMetadata=self.algMetadata)
289 if self.config.doApCorr:
290 self.makeSubtask("applyApCorr", schema=self.measurement.schema)
291 if self.config.doForcedMeasurement:
292 self.schema.addField(
293 "ip_diffim_forced_PsfFlux_instFlux", "D",
294 "Forced PSF flux measured on the direct image.",
295 units="count")
296 self.schema.addField(
297 "ip_diffim_forced_PsfFlux_instFluxErr", "D",
298 "Forced PSF flux error measured on the direct image.",
299 units="count")
300 self.schema.addField(
301 "ip_diffim_forced_PsfFlux_area", "F",
302 "Forced PSF flux effective area of PSF.",
303 units="pixel")
304 self.schema.addField(
305 "ip_diffim_forced_PsfFlux_flag", "Flag",
306 "Forced PSF flux general failure flag.")
307 self.schema.addField(
308 "ip_diffim_forced_PsfFlux_flag_noGoodPixels", "Flag",
309 "Forced PSF flux not enough non-rejected pixels in data to attempt the fit.")
310 self.schema.addField(
311 "ip_diffim_forced_PsfFlux_flag_edge", "Flag",
312 "Forced PSF flux object was too close to the edge of the image to use the full PSF model.")
313 self.makeSubtask("forcedMeasurement", refSchema=self.schema)
314
315 self.schema.addField("refMatchId", "L", "unique id of reference catalog match")
316 self.schema.addField("srcMatchId", "L", "unique id of source match")
317 if self.config.doSkySources:
318 self.makeSubtask("skySources", schema=self.schema)
319 if self.config.doMaskStreaks:
320 self.makeSubtask("maskStreaks")
321 self.makeSubtask("streakDetection")
322
323 # Check that the schema and config are consistent
324 for flag in self.config.badSourceFlags:
325 if flag not in self.schema:
326 raise pipeBase.InvalidQuantumError("Field %s not in schema" % flag)
327
328 # initialize InitOutputs
329 self.outputSchema = afwTable.SourceCatalog(self.schema)
330 self.outputSchema.getTable().setMetadata(self.algMetadata)
331
332 def runQuantum(self, butlerQC: pipeBase.QuantumContext,
333 inputRefs: pipeBase.InputQuantizedConnection,
334 outputRefs: pipeBase.OutputQuantizedConnection):
335 inputs = butlerQC.get(inputRefs)
336 idGenerator = self.config.idGenerator.apply(butlerQC.quantum.dataId)
337 idFactory = idGenerator.make_table_id_factory()
338 outputs = self.run(**inputs, idFactory=idFactory)
339 butlerQC.put(outputs, outputRefs)
340
341 @timeMethod
342 def run(self, science, matchedTemplate, difference,
343 idFactory=None):
344 """Detect and measure sources on a difference image.
345
346 The difference image will be convolved with a gaussian approximation of
347 the PSF to form a maximum likelihood image for detection.
348 Close positive and negative detections will optionally be merged into
349 dipole diaSources.
350 Sky sources, or forced detections in background regions, will optionally
351 be added, and the configured measurement algorithm will be run on all
352 detections.
353
354 Parameters
355 ----------
356 science : `lsst.afw.image.ExposureF`
357 Science exposure that the template was subtracted from.
358 matchedTemplate : `lsst.afw.image.ExposureF`
359 Warped and PSF-matched template that was used produce the
360 difference image.
361 difference : `lsst.afw.image.ExposureF`
362 Result of subtracting template from the science image.
363 idFactory : `lsst.afw.table.IdFactory`, optional
364 Generator object used to assign ids to detected sources in the
365 difference image. Ids from this generator are not set until after
366 deblending and merging positive/negative peaks.
367
368 Returns
369 -------
370 measurementResults : `lsst.pipe.base.Struct`
371
372 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
373 Subtracted exposure with detection mask applied.
374 ``diaSources`` : `lsst.afw.table.SourceCatalog`
375 The catalog of detected sources.
376 """
377 if idFactory is None:
378 idFactory = lsst.meas.base.IdGenerator().make_table_id_factory()
379
380 self._prepareInputs(difference)
381
382 # Don't use the idFactory until after deblend+merge, so that we aren't
383 # generating ids that just get thrown away (footprint merge doesn't
384 # know about past ids).
385 table = afwTable.SourceTable.make(self.schema)
386 results = self.detection.run(
387 table=table,
388 exposure=difference,
389 doSmooth=True,
390 )
391
392 sources, positives, negatives = self._deblend(difference,
393 results.positive,
394 results.negative)
395
396 return self.processResults(science, matchedTemplate, difference, sources, idFactory,
397 positiveFootprints=positives,
398 negativeFootprints=negatives)
399
400 def _prepareInputs(self, difference):
401 """Ensure that we start with an empty detection and deblended mask.
402
403 Parameters
404 ----------
405 difference : `lsst.afw.image.ExposureF`
406 The difference image that will be used for detecting diaSources.
407 The mask plane will be modified in place.
408
409 Raises
410 ------
411 lsst.pipe.base.UpstreamFailureNoWorkFound
412 If the PSF is not usable for measurement.
413 """
414 # Check that we have a valid PSF now before we do more work
415 sigma = difference.psf.computeShape(difference.psf.getAveragePosition()).getDeterminantRadius()
416 if np.isnan(sigma):
417 raise pipeBase.UpstreamFailureNoWorkFound("Invalid PSF detected! PSF width evaluates to NaN.")
418 # Ensure that we start with an empty detection and deblended mask.
419 mask = difference.mask
420 for mp in self.config.clearMaskPlanes:
421 if mp not in mask.getMaskPlaneDict():
422 mask.addMaskPlane(mp)
423 mask &= ~mask.getPlaneBitMask(self.config.clearMaskPlanes)
424
425 def processResults(self, science, matchedTemplate, difference, sources, idFactory,
426 positiveFootprints=None, negativeFootprints=None,):
427 """Measure and process the results of source detection.
428
429 Parameters
430 ----------
431 science : `lsst.afw.image.ExposureF`
432 Science exposure that the template was subtracted from.
433 matchedTemplate : `lsst.afw.image.ExposureF`
434 Warped and PSF-matched template that was used produce the
435 difference image.
436 difference : `lsst.afw.image.ExposureF`
437 Result of subtracting template from the science image.
438 sources : `lsst.afw.table.SourceCatalog`
439 Detected sources on the difference exposure.
440 idFactory : `lsst.afw.table.IdFactory`
441 Generator object used to assign ids to detected sources in the
442 difference image.
443 positiveFootprints : `lsst.afw.detection.FootprintSet`, optional
444 Positive polarity footprints.
445 negativeFootprints : `lsst.afw.detection.FootprintSet`, optional
446 Negative polarity footprints.
447
448 Returns
449 -------
450 measurementResults : `lsst.pipe.base.Struct`
451
452 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
453 Subtracted exposure with detection mask applied.
454 ``diaSources`` : `lsst.afw.table.SourceCatalog`
455 The catalog of detected sources.
456 """
457 self.metadata["nUnmergedDiaSources"] = len(sources)
458 if self.config.doMerge:
459 fpSet = positiveFootprints
460 fpSet.merge(negativeFootprints, self.config.growFootprint,
461 self.config.growFootprint, False)
462 initialDiaSources = afwTable.SourceCatalog(self.schema)
463 fpSet.makeSources(initialDiaSources)
464 self.log.info("Merging detections into %d sources", len(initialDiaSources))
465 else:
466 initialDiaSources = sources
467
468 # Assign source ids at the end: deblend/merge mean that we don't keep
469 # track of parents and children, we only care about the final ids.
470 for source in initialDiaSources:
471 source.setId(idFactory())
472 # Ensure sources added after this get correct ids.
473 initialDiaSources.getTable().setIdFactory(idFactory)
474 initialDiaSources.setMetadata(self.algMetadata)
475
476 self.metadata["nMergedDiaSources"] = len(initialDiaSources)
477
478 if self.config.doMaskStreaks:
479 streakInfo = self._runStreakMasking(difference)
480
481 if self.config.doSkySources:
482 self.addSkySources(initialDiaSources, difference.mask, difference.info.id)
483
484 if not initialDiaSources.isContiguous():
485 initialDiaSources = initialDiaSources.copy(deep=True)
486
487 self.measureDiaSources(initialDiaSources, science, difference, matchedTemplate)
488 diaSources = self._removeBadSources(initialDiaSources)
489
490 if self.config.doForcedMeasurement:
491 self.measureForcedSources(diaSources, science, difference.getWcs())
492
493 self.calculateMetrics(difference)
494
495 measurementResults = pipeBase.Struct(
496 subtractedMeasuredExposure=difference,
497 diaSources=diaSources,
498 )
499 if self.config.doMaskStreaks and self.config.writeStreakInfo:
500 measurementResults.mergeItems(streakInfo, 'maskedStreaks')
501
502 return measurementResults
503
504 def _deblend(self, difference, positiveFootprints, negativeFootprints):
505 """Deblend the positive and negative footprints and return a catalog
506 containing just the children, and the deblended footprints.
507
508 Parameters
509 ----------
510 difference : `lsst.afw.image.Exposure`
511 Result of subtracting template from the science image.
512 positiveFootprints, negativeFootprints : `lsst.afw.detection.FootprintSet`
513 Positive and negative polarity footprints measured on
514 ``difference`` to be deblended separately.
515
516 Returns
517 -------
518 sources : `lsst.afw.table.SourceCatalog`
519 Positive and negative deblended children.
520 positives, negatives : `lsst.afw.detection.FootprintSet`
521 Deblended positive and negative polarity footprints measured on
522 ``difference``.
523 """
524 def makeFootprints(sources):
525 footprints = afwDetection.FootprintSet(difference.getBBox())
526 footprints.setFootprints([src.getFootprint() for src in sources])
527 return footprints
528
529 def deblend(footprints):
530 """Deblend a positive or negative footprint set,
531 and return the deblended children.
532 """
533 sources = afwTable.SourceCatalog(self.schema)
534 footprints.makeSources(sources)
535 self.deblend.run(exposure=difference, sources=sources)
536 self.setPrimaryFlags.run(sources)
537 children = sources["detect_isDeblendedSource"] == 1
538 sources = sources[children].copy(deep=True)
539 # Clear parents, so that measurement plugins behave correctly.
540 sources['parent'] = 0
541 return sources.copy(deep=True)
542
543 positives = deblend(positiveFootprints)
544 negatives = deblend(negativeFootprints)
545
546 sources = afwTable.SourceCatalog(self.schema)
547 sources.reserve(len(positives) + len(negatives))
548 sources.extend(positives, deep=True)
549 sources.extend(negatives, deep=True)
550 return sources, makeFootprints(positives), makeFootprints(negatives)
551
552 def _removeBadSources(self, diaSources):
553 """Remove unphysical diaSources from the catalog.
554
555 Parameters
556 ----------
557 diaSources : `lsst.afw.table.SourceCatalog`
558 The catalog of detected sources.
559
560 Returns
561 -------
562 diaSources : `lsst.afw.table.SourceCatalog`
563 The updated catalog of detected sources, with any source that has a
564 flag in ``config.badSourceFlags`` set removed.
565 """
566 selector = np.ones(len(diaSources), dtype=bool)
567 for flag in self.config.badSourceFlags:
568 flags = diaSources[flag]
569 nBad = np.count_nonzero(flags)
570 if nBad > 0:
571 self.log.debug("Found %d unphysical sources with flag %s.", nBad, flag)
572 selector &= ~flags
573 nBadTotal = np.count_nonzero(~selector)
574 self.metadata["nRemovedBadFlaggedSources"] = nBadTotal
575 self.log.info("Removed %d unphysical sources.", nBadTotal)
576 return diaSources[selector].copy(deep=True)
577
578 def addSkySources(self, diaSources, mask, seed,
579 subtask=None):
580 """Add sources in empty regions of the difference image
581 for measuring the background.
582
583 Parameters
584 ----------
585 diaSources : `lsst.afw.table.SourceCatalog`
586 The catalog of detected sources.
587 mask : `lsst.afw.image.Mask`
588 Mask plane for determining regions where Sky sources can be added.
589 seed : `int`
590 Seed value to initialize the random number generator.
591 """
592 if subtask is None:
593 subtask = self.skySources
594 skySourceFootprints = subtask.run(mask=mask, seed=seed, catalog=diaSources)
595 self.metadata[f"n_{subtask.getName()}"] = len(skySourceFootprints)
596
597 def measureDiaSources(self, diaSources, science, difference, matchedTemplate):
598 """Use (matched) template and science image to constrain dipole fitting.
599
600 Parameters
601 ----------
602 diaSources : `lsst.afw.table.SourceCatalog`
603 The catalog of detected sources.
604 science : `lsst.afw.image.ExposureF`
605 Science exposure that the template was subtracted from.
606 difference : `lsst.afw.image.ExposureF`
607 Result of subtracting template from the science image.
608 matchedTemplate : `lsst.afw.image.ExposureF`
609 Warped and PSF-matched template that was used produce the
610 difference image.
611 """
612 # Ensure that the required mask planes are present
613 for mp in self.config.measurement.plugins["base_PixelFlags"].masksFpAnywhere:
614 difference.mask.addMaskPlane(mp)
615 # Note that this may not be correct if we convolved the science image.
616 # In the future we may wish to persist the matchedScience image.
617 self.measurement.run(diaSources, difference, science, matchedTemplate)
618 if self.config.doApCorr:
619 apCorrMap = difference.getInfo().getApCorrMap()
620 if apCorrMap is None:
621 self.log.warning("Difference image does not have valid aperture correction; skipping.")
622 else:
623 self.applyApCorr.run(
624 catalog=diaSources,
625 apCorrMap=apCorrMap,
626 )
627
628 def measureForcedSources(self, diaSources, science, wcs):
629 """Perform forced measurement of the diaSources on the science image.
630
631 Parameters
632 ----------
633 diaSources : `lsst.afw.table.SourceCatalog`
634 The catalog of detected sources.
635 science : `lsst.afw.image.ExposureF`
636 Science exposure that the template was subtracted from.
637 wcs : `lsst.afw.geom.SkyWcs`
638 Coordinate system definition (wcs) for the exposure.
639 """
640 # Run forced psf photometry on the PVI at the diaSource locations.
641 # Copy the measured flux and error into the diaSource.
642 forcedSources = self.forcedMeasurement.generateMeasCat(science, diaSources, wcs)
643 self.forcedMeasurement.run(forcedSources, science, diaSources, wcs)
644 mapper = afwTable.SchemaMapper(forcedSources.schema, diaSources.schema)
645 mapper.addMapping(forcedSources.schema.find("base_PsfFlux_instFlux")[0],
646 "ip_diffim_forced_PsfFlux_instFlux", True)
647 mapper.addMapping(forcedSources.schema.find("base_PsfFlux_instFluxErr")[0],
648 "ip_diffim_forced_PsfFlux_instFluxErr", True)
649 mapper.addMapping(forcedSources.schema.find("base_PsfFlux_area")[0],
650 "ip_diffim_forced_PsfFlux_area", True)
651 mapper.addMapping(forcedSources.schema.find("base_PsfFlux_flag")[0],
652 "ip_diffim_forced_PsfFlux_flag", True)
653 mapper.addMapping(forcedSources.schema.find("base_PsfFlux_flag_noGoodPixels")[0],
654 "ip_diffim_forced_PsfFlux_flag_noGoodPixels", True)
655 mapper.addMapping(forcedSources.schema.find("base_PsfFlux_flag_edge")[0],
656 "ip_diffim_forced_PsfFlux_flag_edge", True)
657 for diaSource, forcedSource in zip(diaSources, forcedSources):
658 diaSource.assign(forcedSource, mapper)
659
660 def calculateMetrics(self, difference):
661 """Add difference image QA metrics to the Task metadata.
662
663 This may be used to produce corresponding metrics (see
664 lsst.analysis.tools.tasks.diffimTaskDetectorVisitMetricAnalysis).
665
666 Parameters
667 ----------
668 difference : `lsst.afw.image.Exposure`
669 The target difference image to calculate metrics for.
670 """
671 mask = difference.mask
672 badPix = (mask.array & mask.getPlaneBitMask(self.config.detection.excludeMaskPlanes)) > 0
673 self.metadata["nGoodPixels"] = np.sum(~badPix)
674 self.metadata["nBadPixels"] = np.sum(badPix)
675 detPosPix = (mask.array & mask.getPlaneBitMask("DETECTED")) > 0
676 detNegPix = (mask.array & mask.getPlaneBitMask("DETECTED_NEGATIVE")) > 0
677 self.metadata["nPixelsDetectedPositive"] = np.sum(detPosPix)
678 self.metadata["nPixelsDetectedNegative"] = np.sum(detNegPix)
679 detPosPix &= badPix
680 detNegPix &= badPix
681 self.metadata["nBadPixelsDetectedPositive"] = np.sum(detPosPix)
682 self.metadata["nBadPixelsDetectedNegative"] = np.sum(detNegPix)
683
684 metricsMaskPlanes = list(mask.getMaskPlaneDict().keys())
685 for maskPlane in metricsMaskPlanes:
686 try:
687 self.metadata["%s_mask_fraction"%maskPlane.lower()] = evaluateMaskFraction(mask, maskPlane)
688 except InvalidParameterError:
689 self.metadata["%s_mask_fraction"%maskPlane.lower()] = -1
690 self.log.info("Unable to calculate metrics for mask plane %s: not in image"%maskPlane)
691
692 def _runStreakMasking(self, difference):
693 """Do streak masking and optionally save the resulting streak
694 fit parameters in a catalog.
695
696 Only returns non-empty streakInfo if self.config.writeStreakInfo
697 is set. The difference image is binned by self.config.streakBinFactor
698 (and detection is run a second time) so that regions with lower
699 surface brightness streaks are more likely to fall above the
700 detection threshold.
701
702 Parameters
703 ----------
704 difference: `lsst.afw.image.Exposure`
705 The exposure in which to search for streaks. Must have a detection
706 mask.
707
708 Returns
709 -------
710 streakInfo: `lsst.pipe.base.Struct`
711 ``rho`` : `np.ndarray`
712 Angle of detected streak.
713 ``theta`` : `np.ndarray`
714 Distance from center of detected streak.
715 ``sigma`` : `np.ndarray`
716 Width of streak profile.
717 ``reducedChi2`` : `np.ndarray`
718 Reduced chi2 of the best-fit streak profile.
719 ``modelMaximum`` : `np.ndarray`
720 Peak value of the fit line profile.
721 """
722 maskedImage = difference.maskedImage
723 # Bin the diffim to enhance low surface brightness streaks
724 binnedMaskedImage = afwMath.binImage(maskedImage,
725 self.config.streakBinFactor,
726 self.config.streakBinFactor)
727 binnedExposure = afwImage.ExposureF(binnedMaskedImage.getBBox())
728 binnedExposure.setMaskedImage(binnedMaskedImage)
729 # Clear the DETECTED mask plane before streak detection
730 binnedExposure.mask &= ~binnedExposure.mask.getPlaneBitMask('DETECTED')
731 # Rerun detection to set the DETECTED mask plane on binnedExposure
732 sigma = difference.psf.computeShape(difference.psf.getAveragePosition()).getDeterminantRadius()
733 _table = afwTable.SourceTable.make(afwTable.SourceTable.makeMinimalSchema())
734 self.streakDetection.run(table=_table, exposure=binnedExposure, doSmooth=True,
735 sigma=sigma/self.config.streakBinFactor)
736 binnedDetectedMaskPlane = binnedExposure.mask.array & binnedExposure.mask.getPlaneBitMask('DETECTED')
737 rescaledDetectedMaskPlane = binnedDetectedMaskPlane.repeat(self.config.streakBinFactor,
738 axis=0).repeat(self.config.streakBinFactor,
739 axis=1)
740 # Create new version of a diffim with DETECTED based on binnedExposure
741 streakMaskedImage = maskedImage.clone()
742 ysize, xsize = rescaledDetectedMaskPlane.shape
743 streakMaskedImage.mask.array[:ysize, :xsize] |= rescaledDetectedMaskPlane
744 # Detect streaks on this new version of the diffim
745 streaks = self.maskStreaks.run(streakMaskedImage)
746 streakMaskPlane = streakMaskedImage.mask.array & streakMaskedImage.mask.getPlaneBitMask('STREAK')
747 # Apply the new STREAK mask to the original diffim
748 maskedImage.mask.array |= streakMaskPlane
749
750 if self.config.writeStreakInfo:
751 rhos = np.array([line.rho for line in streaks.lines])
752 thetas = np.array([line.theta for line in streaks.lines])
753 sigmas = np.array([line.sigma for line in streaks.lines])
754 chi2s = np.array([line.reducedChi2 for line in streaks.lines])
755 modelMaximums = np.array([line.modelMaximum for line in streaks.lines])
756 streakInfo = {'rho': rhos, 'theta': thetas, 'sigma': sigmas, 'reducedChi2': chi2s,
757 'modelMaximum': modelMaximums}
758 else:
759 streakInfo = {'rho': np.array([]), 'theta': np.array([]), 'sigma': np.array([]),
760 'reducedChi2': np.array([]), 'modelMaximum': np.array([])}
761 return pipeBase.Struct(maskedStreaks=streakInfo)
762
763
764class DetectAndMeasureScoreConnections(DetectAndMeasureConnections):
765 scoreExposure = pipeBase.connectionTypes.Input(
766 doc="Maximum likelihood image for detection.",
767 dimensions=("instrument", "visit", "detector"),
768 storageClass="ExposureF",
769 name="{fakesType}{coaddName}Diff_scoreExp",
770 )
771
772
773class DetectAndMeasureScoreConfig(DetectAndMeasureConfig,
774 pipelineConnections=DetectAndMeasureScoreConnections):
775 pass
776
777
778class DetectAndMeasureScoreTask(DetectAndMeasureTask):
779 """Detect DIA sources using a score image,
780 and measure the detections on the difference image.
781
782 Source detection is run on the supplied score, or maximum likelihood,
783 image. Note that no additional convolution will be done in this case.
784 Close positive and negative detections will optionally be merged into
785 dipole diaSources.
786 Sky sources, or forced detections in background regions, will optionally
787 be added, and the configured measurement algorithm will be run on all
788 detections.
789 """
790 ConfigClass = DetectAndMeasureScoreConfig
791 _DefaultName = "detectAndMeasureScore"
792
793 @timeMethod
794 def run(self, science, matchedTemplate, difference, scoreExposure,
795 idFactory=None):
796 """Detect and measure sources on a score image.
797
798 Parameters
799 ----------
800 science : `lsst.afw.image.ExposureF`
801 Science exposure that the template was subtracted from.
802 matchedTemplate : `lsst.afw.image.ExposureF`
803 Warped and PSF-matched template that was used produce the
804 difference image.
805 difference : `lsst.afw.image.ExposureF`
806 Result of subtracting template from the science image.
807 scoreExposure : `lsst.afw.image.ExposureF`
808 Score or maximum likelihood difference image
809 idFactory : `lsst.afw.table.IdFactory`, optional
810 Generator object used to assign ids to detected sources in the
811 difference image. Ids from this generator are not set until after
812 deblending and merging positive/negative peaks.
813
814 Returns
815 -------
816 measurementResults : `lsst.pipe.base.Struct`
817
818 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
819 Subtracted exposure with detection mask applied.
820 ``diaSources`` : `lsst.afw.table.SourceCatalog`
821 The catalog of detected sources.
822 """
823 if idFactory is None:
824 idFactory = lsst.meas.base.IdGenerator().make_table_id_factory()
825
826 self._prepareInputs(scoreExposure)
827
828 # Don't use the idFactory until after deblend+merge, so that we aren't
829 # generating ids that just get thrown away (footprint merge doesn't
830 # know about past ids).
831 table = afwTable.SourceTable.make(self.schema)
832 results = self.detection.run(
833 table=table,
834 exposure=scoreExposure,
835 doSmooth=False,
836 )
837 # Copy the detection mask from the Score image to the difference image
838 difference.mask.assign(scoreExposure.mask, scoreExposure.getBBox())
839
840 sources, positives, negatives = self._deblend(difference,
841 results.positive,
842 results.negative)
843
844 return self.processResults(science, matchedTemplate, difference, sources, idFactory,
845 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.
std::shared_ptr< ImageT > binImage(ImageT const &inImage, int const binX, int const binY, lsst::afw::math::Property const flags=lsst::afw::math::MEAN)
Definition binImage.cc:44
run(self, coaddExposures, bbox, wcs, dataIds, physical_filter)