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