LSST Applications g034a557a3c+dd8dd8f11d,g0afe43252f+b86e4b8053,g11f7dcd041+017865fdd3,g1cd03abf6b+8446defddb,g1ce3e0751c+f991eae79d,g28da252d5a+ca8a1a9fb3,g2bbee38e9b+b6588ad223,g2bc492864f+b6588ad223,g2cdde0e794+8523d0dbb4,g347aa1857d+b6588ad223,g35bb328faa+b86e4b8053,g3a166c0a6a+b6588ad223,g461a3dce89+b86e4b8053,g52b1c1532d+b86e4b8053,g7f3b0d46df+ad13c1b82d,g80478fca09+f29c5d6c70,g858d7b2824+293f439f82,g8cd86fa7b1+af721d2595,g965a9036f2+293f439f82,g979bb04a14+51ed57f74c,g9ddcbc5298+f24b38b85a,gae0086650b+b86e4b8053,gbb886bcc26+b97e247655,gc28159a63d+b6588ad223,gc30aee3386+a2f0f6cab9,gcaf7e4fdec+293f439f82,gcd45df26be+293f439f82,gcdd4ae20e8+70b5def7e6,gce08ada175+da9c58a417,gcf0d15dbbd+70b5def7e6,gdaeeff99f8+006e14e809,gdbce86181e+6a170ce272,ge3d4d395c2+224150c836,ge5f7162a3a+bb2241c923,ge6cb8fbbf7+d119aed356,ge79ae78c31+b6588ad223,gf048a9a2f4+40ffced2b8,gf0baf85859+b4cca3d10f,w.2024.30
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
225class DetectAndMeasureTask(lsst.pipe.base.PipelineTask):
226 """Detect and measure sources on a difference image.
227 """
228 ConfigClass = DetectAndMeasureConfig
229 _DefaultName = "detectAndMeasure"
230
231 def __init__(self, **kwargs):
232 super().__init__(**kwargs)
233 self.schema = afwTable.SourceTable.makeMinimalSchema()
234 # Add coordinate error fields:
235 afwTable.CoordKey.addErrorFields(self.schema)
236
237 self.algMetadata = dafBase.PropertyList()
238 self.makeSubtask("detection", schema=self.schema)
239 self.makeSubtask("deblend", schema=self.schema)
240 self.makeSubtask("setPrimaryFlags", schema=self.schema, isSingleFrame=True)
241 self.makeSubtask("measurement", schema=self.schema,
242 algMetadata=self.algMetadata)
243 if self.config.doApCorr:
244 self.makeSubtask("applyApCorr", schema=self.measurement.schema)
245 if self.config.doForcedMeasurement:
246 self.schema.addField(
247 "ip_diffim_forced_PsfFlux_instFlux", "D",
248 "Forced PSF flux measured on the direct image.",
249 units="count")
250 self.schema.addField(
251 "ip_diffim_forced_PsfFlux_instFluxErr", "D",
252 "Forced PSF flux error measured on the direct image.",
253 units="count")
254 self.schema.addField(
255 "ip_diffim_forced_PsfFlux_area", "F",
256 "Forced PSF flux effective area of PSF.",
257 units="pixel")
258 self.schema.addField(
259 "ip_diffim_forced_PsfFlux_flag", "Flag",
260 "Forced PSF flux general failure flag.")
261 self.schema.addField(
262 "ip_diffim_forced_PsfFlux_flag_noGoodPixels", "Flag",
263 "Forced PSF flux not enough non-rejected pixels in data to attempt the fit.")
264 self.schema.addField(
265 "ip_diffim_forced_PsfFlux_flag_edge", "Flag",
266 "Forced PSF flux object was too close to the edge of the image to use the full PSF model.")
267 self.makeSubtask("forcedMeasurement", refSchema=self.schema)
268
269 self.schema.addField("refMatchId", "L", "unique id of reference catalog match")
270 self.schema.addField("srcMatchId", "L", "unique id of source match")
271 if self.config.doSkySources:
272 self.makeSubtask("skySources", schema=self.schema)
273 if self.config.doMaskStreaks:
274 self.makeSubtask("maskStreaks")
275
276 # Check that the schema and config are consistent
277 for flag in self.config.badSourceFlags:
278 if flag not in self.schema:
279 raise pipeBase.InvalidQuantumError("Field %s not in schema" % flag)
280
281 # initialize InitOutputs
282 self.outputSchema = afwTable.SourceCatalog(self.schema)
283 self.outputSchema.getTable().setMetadata(self.algMetadata)
284
285 def runQuantum(self, butlerQC: pipeBase.QuantumContext,
286 inputRefs: pipeBase.InputQuantizedConnection,
287 outputRefs: pipeBase.OutputQuantizedConnection):
288 inputs = butlerQC.get(inputRefs)
289 idGenerator = self.config.idGenerator.apply(butlerQC.quantum.dataId)
290 idFactory = idGenerator.make_table_id_factory()
291 outputs = self.run(**inputs, idFactory=idFactory)
292 butlerQC.put(outputs, outputRefs)
293
294 @timeMethod
295 def run(self, science, matchedTemplate, difference,
296 idFactory=None):
297 """Detect and measure sources on a difference image.
298
299 The difference image will be convolved with a gaussian approximation of
300 the PSF to form a maximum likelihood image for detection.
301 Close positive and negative detections will optionally be merged into
302 dipole diaSources.
303 Sky sources, or forced detections in background regions, will optionally
304 be added, and the configured measurement algorithm will be run on all
305 detections.
306
307 Parameters
308 ----------
309 science : `lsst.afw.image.ExposureF`
310 Science exposure that the template was subtracted from.
311 matchedTemplate : `lsst.afw.image.ExposureF`
312 Warped and PSF-matched template that was used produce the
313 difference image.
314 difference : `lsst.afw.image.ExposureF`
315 Result of subtracting template from the science image.
316 idFactory : `lsst.afw.table.IdFactory`, optional
317 Generator object used to assign ids to detected sources in the
318 difference image. Ids from this generator are not set until after
319 deblending and merging positive/negative peaks.
320
321 Returns
322 -------
323 measurementResults : `lsst.pipe.base.Struct`
324
325 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
326 Subtracted exposure with detection mask applied.
327 ``diaSources`` : `lsst.afw.table.SourceCatalog`
328 The catalog of detected sources.
329 """
330 if idFactory is None:
331 idFactory = lsst.meas.base.IdGenerator().make_table_id_factory()
332
333 # Ensure that we start with an empty detection and deblended mask.
334 mask = difference.mask
335 clearMaskPlanes = ["DETECTED", "DETECTED_NEGATIVE", "NOT_DEBLENDED", "STREAK"]
336 for mp in clearMaskPlanes:
337 if mp not in mask.getMaskPlaneDict():
338 mask.addMaskPlane(mp)
339 mask &= ~mask.getPlaneBitMask(clearMaskPlanes)
340
341 # Don't use the idFactory until after deblend+merge, so that we aren't
342 # generating ids that just get thrown away (footprint merge doesn't
343 # know about past ids).
344 table = afwTable.SourceTable.make(self.schema)
345 results = self.detection.run(
346 table=table,
347 exposure=difference,
348 doSmooth=True,
349 )
350
351 sources, positives, negatives = self._deblend(difference,
352 results.positive,
353 results.negative)
354
355 return self.processResults(science, matchedTemplate, difference, sources, idFactory,
356 positiveFootprints=positives,
357 negativeFootprints=negatives)
358
359 def processResults(self, science, matchedTemplate, difference, sources, idFactory,
360 positiveFootprints=None, negativeFootprints=None,):
361 """Measure and process the results of source detection.
362
363 Parameters
364 ----------
365 science : `lsst.afw.image.ExposureF`
366 Science exposure that the template was subtracted from.
367 matchedTemplate : `lsst.afw.image.ExposureF`
368 Warped and PSF-matched template that was used produce the
369 difference image.
370 difference : `lsst.afw.image.ExposureF`
371 Result of subtracting template from the science image.
372 sources : `lsst.afw.table.SourceCatalog`
373 Detected sources on the difference exposure.
374 idFactory : `lsst.afw.table.IdFactory`
375 Generator object used to assign ids to detected sources in the
376 difference image.
377 positiveFootprints : `lsst.afw.detection.FootprintSet`, optional
378 Positive polarity footprints.
379 negativeFootprints : `lsst.afw.detection.FootprintSet`, optional
380 Negative polarity footprints.
381
382 Returns
383 -------
384 measurementResults : `lsst.pipe.base.Struct`
385
386 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
387 Subtracted exposure with detection mask applied.
388 ``diaSources`` : `lsst.afw.table.SourceCatalog`
389 The catalog of detected sources.
390 """
391 self.metadata.add("nUnmergedDiaSources", len(sources))
392 if self.config.doMerge:
393 fpSet = positiveFootprints
394 fpSet.merge(negativeFootprints, self.config.growFootprint,
395 self.config.growFootprint, False)
396 initialDiaSources = afwTable.SourceCatalog(self.schema)
397 fpSet.makeSources(initialDiaSources)
398 self.log.info("Merging detections into %d sources", len(initialDiaSources))
399 else:
400 initialDiaSources = sources
401
402 # Assign source ids at the end: deblend/merge mean that we don't keep
403 # track of parents and children, we only care about the final ids.
404 for source in initialDiaSources:
405 source.setId(idFactory())
406 # Ensure sources added after this get correct ids.
407 initialDiaSources.getTable().setIdFactory(idFactory)
408 initialDiaSources.setMetadata(self.algMetadata)
409
410 self.metadata.add("nMergedDiaSources", len(initialDiaSources))
411
412 if self.config.doMaskStreaks:
413 streakInfo = self._runStreakMasking(difference.maskedImage)
414
415 if self.config.doSkySources:
416 self.addSkySources(initialDiaSources, difference.mask, difference.info.id)
417
418 if not initialDiaSources.isContiguous():
419 initialDiaSources = initialDiaSources.copy(deep=True)
420
421 self.measureDiaSources(initialDiaSources, science, difference, matchedTemplate)
422 diaSources = self._removeBadSources(initialDiaSources)
423
424 if self.config.doForcedMeasurement:
425 self.measureForcedSources(diaSources, science, difference.getWcs())
426
427 self.calculateMetrics(difference)
428
429 measurementResults = pipeBase.Struct(
430 subtractedMeasuredExposure=difference,
431 diaSources=diaSources,
432 )
433 if self.config.doMaskStreaks and self.config.writeStreakInfo:
434 measurementResults.mergeItems(streakInfo, 'maskedStreaks')
435
436 return measurementResults
437
438 def _deblend(self, difference, positiveFootprints, negativeFootprints):
439 """Deblend the positive and negative footprints and return a catalog
440 containing just the children, and the deblended footprints.
441
442 Parameters
443 ----------
444 difference : `lsst.afw.image.Exposure`
445 Result of subtracting template from the science image.
446 positiveFootprints, negativeFootprints : `lsst.afw.detection.FootprintSet`
447 Positive and negative polarity footprints measured on
448 ``difference`` to be deblended separately.
449
450 Returns
451 -------
452 sources : `lsst.afw.table.SourceCatalog`
453 Positive and negative deblended children.
454 positives, negatives : `lsst.afw.detection.FootprintSet`
455 Deblended positive and negative polarity footprints measured on
456 ``difference``.
457 """
458 def makeFootprints(sources):
459 footprints = afwDetection.FootprintSet(difference.getBBox())
460 footprints.setFootprints([src.getFootprint() for src in sources])
461 return footprints
462
463 def deblend(footprints):
464 """Deblend a positive or negative footprint set,
465 and return the deblended children.
466 """
467 sources = afwTable.SourceCatalog(self.schema)
468 footprints.makeSources(sources)
469 self.deblend.run(exposure=difference, sources=sources)
470 self.setPrimaryFlags.run(sources)
471 children = sources["detect_isDeblendedSource"] == 1
472 sources = sources[children].copy(deep=True)
473 # Clear parents, so that measurement plugins behave correctly.
474 sources['parent'] = 0
475 return sources.copy(deep=True)
476
477 positives = deblend(positiveFootprints)
478 negatives = deblend(negativeFootprints)
479
480 sources = afwTable.SourceCatalog(self.schema)
481 sources.reserve(len(positives) + len(negatives))
482 sources.extend(positives, deep=True)
483 sources.extend(negatives, deep=True)
484 return sources, makeFootprints(positives), makeFootprints(negatives)
485
486 def _removeBadSources(self, diaSources):
487 """Remove unphysical diaSources from the catalog.
488
489 Parameters
490 ----------
491 diaSources : `lsst.afw.table.SourceCatalog`
492 The catalog of detected sources.
493
494 Returns
495 -------
496 diaSources : `lsst.afw.table.SourceCatalog`
497 The updated catalog of detected sources, with any source that has a
498 flag in ``config.badSourceFlags`` set removed.
499 """
500 selector = np.ones(len(diaSources), dtype=bool)
501 for flag in self.config.badSourceFlags:
502 flags = diaSources[flag]
503 nBad = np.count_nonzero(flags)
504 if nBad > 0:
505 self.log.debug("Found %d unphysical sources with flag %s.", nBad, flag)
506 selector &= ~flags
507 nBadTotal = np.count_nonzero(~selector)
508 self.metadata.add("nRemovedBadFlaggedSources", nBadTotal)
509 self.log.info("Removed %d unphysical sources.", nBadTotal)
510 return diaSources[selector].copy(deep=True)
511
512 def addSkySources(self, diaSources, mask, seed,
513 subtask=None):
514 """Add sources in empty regions of the difference image
515 for measuring the background.
516
517 Parameters
518 ----------
519 diaSources : `lsst.afw.table.SourceCatalog`
520 The catalog of detected sources.
521 mask : `lsst.afw.image.Mask`
522 Mask plane for determining regions where Sky sources can be added.
523 seed : `int`
524 Seed value to initialize the random number generator.
525 """
526 if subtask is None:
527 subtask = self.skySources
528 skySourceFootprints = subtask.run(mask=mask, seed=seed, catalog=diaSources)
529 self.metadata.add(f"n_{subtask.getName()}", len(skySourceFootprints))
530
531 def measureDiaSources(self, diaSources, science, difference, matchedTemplate):
532 """Use (matched) template and science image to constrain dipole fitting.
533
534 Parameters
535 ----------
536 diaSources : `lsst.afw.table.SourceCatalog`
537 The catalog of detected sources.
538 science : `lsst.afw.image.ExposureF`
539 Science exposure that the template was subtracted from.
540 difference : `lsst.afw.image.ExposureF`
541 Result of subtracting template from the science image.
542 matchedTemplate : `lsst.afw.image.ExposureF`
543 Warped and PSF-matched template that was used produce the
544 difference image.
545 """
546 # Ensure that the required mask planes are present
547 for mp in self.config.measurement.plugins["base_PixelFlags"].masksFpAnywhere:
548 difference.mask.addMaskPlane(mp)
549 # Note that this may not be correct if we convolved the science image.
550 # In the future we may wish to persist the matchedScience image.
551 self.measurement.run(diaSources, difference, science, matchedTemplate)
552 if self.config.doApCorr:
553 apCorrMap = difference.getInfo().getApCorrMap()
554 if apCorrMap is None:
555 self.log.warning("Difference image does not have valid aperture correction; skipping.")
556 else:
557 self.applyApCorr.run(
558 catalog=diaSources,
559 apCorrMap=apCorrMap,
560 )
561
562 def measureForcedSources(self, diaSources, science, wcs):
563 """Perform forced measurement of the diaSources on the science image.
564
565 Parameters
566 ----------
567 diaSources : `lsst.afw.table.SourceCatalog`
568 The catalog of detected sources.
569 science : `lsst.afw.image.ExposureF`
570 Science exposure that the template was subtracted from.
571 wcs : `lsst.afw.geom.SkyWcs`
572 Coordinate system definition (wcs) for the exposure.
573 """
574 # Run forced psf photometry on the PVI at the diaSource locations.
575 # Copy the measured flux and error into the diaSource.
576 forcedSources = self.forcedMeasurement.generateMeasCat(science, diaSources, wcs)
577 self.forcedMeasurement.run(forcedSources, science, diaSources, wcs)
578 mapper = afwTable.SchemaMapper(forcedSources.schema, diaSources.schema)
579 mapper.addMapping(forcedSources.schema.find("base_PsfFlux_instFlux")[0],
580 "ip_diffim_forced_PsfFlux_instFlux", True)
581 mapper.addMapping(forcedSources.schema.find("base_PsfFlux_instFluxErr")[0],
582 "ip_diffim_forced_PsfFlux_instFluxErr", True)
583 mapper.addMapping(forcedSources.schema.find("base_PsfFlux_area")[0],
584 "ip_diffim_forced_PsfFlux_area", True)
585 mapper.addMapping(forcedSources.schema.find("base_PsfFlux_flag")[0],
586 "ip_diffim_forced_PsfFlux_flag", True)
587 mapper.addMapping(forcedSources.schema.find("base_PsfFlux_flag_noGoodPixels")[0],
588 "ip_diffim_forced_PsfFlux_flag_noGoodPixels", True)
589 mapper.addMapping(forcedSources.schema.find("base_PsfFlux_flag_edge")[0],
590 "ip_diffim_forced_PsfFlux_flag_edge", True)
591 for diaSource, forcedSource in zip(diaSources, forcedSources):
592 diaSource.assign(forcedSource, mapper)
593
594 def calculateMetrics(self, difference):
595 """Add difference image QA metrics to the Task metadata.
596
597 This may be used to produce corresponding metrics (see
598 lsst.analysis.tools.tasks.diffimTaskDetectorVisitMetricAnalysis).
599
600 Parameters
601 ----------
602 difference : `lsst.afw.image.Exposure`
603 The target difference image to calculate metrics for.
604 """
605 mask = difference.mask
606 badPix = (mask.array & mask.getPlaneBitMask(self.config.detection.excludeMaskPlanes)) > 0
607 self.metadata.add("nGoodPixels", np.sum(~badPix))
608 self.metadata.add("nBadPixels", np.sum(badPix))
609 detPosPix = (mask.array & mask.getPlaneBitMask("DETECTED")) > 0
610 detNegPix = (mask.array & mask.getPlaneBitMask("DETECTED_NEGATIVE")) > 0
611 self.metadata.add("nPixelsDetectedPositive", np.sum(detPosPix))
612 self.metadata.add("nPixelsDetectedNegative", np.sum(detNegPix))
613 detPosPix &= badPix
614 detNegPix &= badPix
615 self.metadata.add("nBadPixelsDetectedPositive", np.sum(detPosPix))
616 self.metadata.add("nBadPixelsDetectedNegative", np.sum(detNegPix))
617
618 metricsMaskPlanes = list(mask.getMaskPlaneDict().keys())
619 for maskPlane in metricsMaskPlanes:
620 try:
621 self.metadata.add("%s_mask_fraction"%maskPlane.lower(), evaluateMaskFraction(mask, maskPlane))
622 except InvalidParameterError:
623 self.metadata.add("%s_mask_fraction"%maskPlane.lower(), -1)
624 self.log.info("Unable to calculate metrics for mask plane %s: not in image"%maskPlane)
625
626 def _runStreakMasking(self, maskedImage):
627 """Do streak masking and optionally save the resulting streak
628 fit parameters in a catalog.
629
630 Parameters
631 ----------
632 maskedImage: `lsst.afw.image.maskedImage`
633 The image in which to search for streaks. Must have a detection
634 mask.
635
636 Returns
637 -------
638 streakInfo: `lsst.pipe.base.Struct`
639 ``rho`` : `np.ndarray`
640 Angle of detected streak.
641 ``theta`` : `np.ndarray`
642 Distance from center of detected streak.
643 ``sigma`` : `np.ndarray`
644 Width of streak profile.
645 """
646 streaks = self.maskStreaks.run(maskedImage)
647 if self.config.writeStreakInfo:
648 rhos = np.array([line.rho for line in streaks.lines])
649 thetas = np.array([line.theta for line in streaks.lines])
650 sigmas = np.array([line.sigma for line in streaks.lines])
651 streakInfo = {'rho': rhos, 'theta': thetas, 'sigma': sigmas}
652 else:
653 streakInfo = {'rho': np.array([]), 'theta': np.array([]), 'sigma': np.array([])}
654 return pipeBase.Struct(maskedStreaks=streakInfo)
655
656
657class DetectAndMeasureScoreConnections(DetectAndMeasureConnections):
658 scoreExposure = pipeBase.connectionTypes.Input(
659 doc="Maximum likelihood image for detection.",
660 dimensions=("instrument", "visit", "detector"),
661 storageClass="ExposureF",
662 name="{fakesType}{coaddName}Diff_scoreExp",
663 )
664
665
666class DetectAndMeasureScoreConfig(DetectAndMeasureConfig,
667 pipelineConnections=DetectAndMeasureScoreConnections):
668 pass
669
670
671class DetectAndMeasureScoreTask(DetectAndMeasureTask):
672 """Detect DIA sources using a score image,
673 and measure the detections on the difference image.
674
675 Source detection is run on the supplied score, or maximum likelihood,
676 image. Note that no additional convolution will be done in this case.
677 Close positive and negative detections will optionally be merged into
678 dipole diaSources.
679 Sky sources, or forced detections in background regions, will optionally
680 be added, and the configured measurement algorithm will be run on all
681 detections.
682 """
683 ConfigClass = DetectAndMeasureScoreConfig
684 _DefaultName = "detectAndMeasureScore"
685
686 @timeMethod
687 def run(self, science, matchedTemplate, difference, scoreExposure,
688 idFactory=None):
689 """Detect and measure sources on a score image.
690
691 Parameters
692 ----------
693 science : `lsst.afw.image.ExposureF`
694 Science exposure that the template was subtracted from.
695 matchedTemplate : `lsst.afw.image.ExposureF`
696 Warped and PSF-matched template that was used produce the
697 difference image.
698 difference : `lsst.afw.image.ExposureF`
699 Result of subtracting template from the science image.
700 scoreExposure : `lsst.afw.image.ExposureF`
701 Score or maximum likelihood difference image
702 idFactory : `lsst.afw.table.IdFactory`, optional
703 Generator object used to assign ids to detected sources in the
704 difference image. Ids from this generator are not set until after
705 deblending and merging positive/negative peaks.
706
707 Returns
708 -------
709 measurementResults : `lsst.pipe.base.Struct`
710
711 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
712 Subtracted exposure with detection mask applied.
713 ``diaSources`` : `lsst.afw.table.SourceCatalog`
714 The catalog of detected sources.
715 """
716 if idFactory is None:
717 idFactory = lsst.meas.base.IdGenerator().make_table_id_factory()
718
719 # Ensure that we start with an empty detection mask.
720 mask = scoreExposure.mask
721 mask &= ~(mask.getPlaneBitMask("DETECTED") | mask.getPlaneBitMask("DETECTED_NEGATIVE"))
722
723 # Don't use the idFactory until after deblend+merge, so that we aren't
724 # generating ids that just get thrown away (footprint merge doesn't
725 # know about past ids).
726 table = afwTable.SourceTable.make(self.schema)
727 results = self.detection.run(
728 table=table,
729 exposure=scoreExposure,
730 doSmooth=False,
731 )
732 # Copy the detection mask from the Score image to the difference image
733 difference.mask.assign(scoreExposure.mask, scoreExposure.getBBox())
734
735 sources, positives, negatives = self._deblend(difference,
736 results.positive,
737 results.negative)
738
739 return self.processResults(science, matchedTemplate, difference, sources, idFactory,
740 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)