28from lsst.ip.diffim.utils
import getPsfFwhm, angleMean
29from lsst.meas.algorithms
import SkyObjectsTask, SourceDetectionTask, SetPrimaryFlagsTask
30from lsst.meas.base import ForcedMeasurementTask, ApplyApCorrTask, DetectorVisitIdGeneratorConfig
31import lsst.meas.deblender
32import lsst.meas.extensions.trailedSources
33import lsst.meas.extensions.shapeHSM
38from lsst.utils.timer
import timeMethod
40from .
import DipoleFitTask
42__all__ = [
"DetectAndMeasureConfig",
"DetectAndMeasureTask",
43 "DetectAndMeasureScoreConfig",
"DetectAndMeasureScoreTask"]
47 dimensions=(
"instrument",
"visit",
"detector"),
48 defaultTemplates={
"coaddName":
"deep",
51 science = pipeBase.connectionTypes.Input(
52 doc=
"Input science exposure.",
53 dimensions=(
"instrument",
"visit",
"detector"),
54 storageClass=
"ExposureF",
55 name=
"{fakesType}calexp"
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",
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",
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",
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",
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",
86 spatiallySampledMetrics = pipeBase.connectionTypes.Output(
87 doc=
"Summary metrics computed at randomized locations.",
88 dimensions=(
"instrument",
"visit",
"detector"),
89 storageClass=
"ArrowAstropy",
90 name=
"{fakesType}{coaddName}Diff_spatiallySampledMetrics",
93 def __init__(self, *, config=None):
94 super().__init__(config=config)
95 if not config.doWriteMetrics:
96 self.outputs.remove(
"spatiallySampledMetrics")
100 pipelineConnections=DetectAndMeasureConnections):
101 """Config for DetectAndMeasureTask
103 doMerge = pexConfig.Field(
106 doc=
"Merge positive and negative diaSources with grow radius "
107 "set by growFootprint"
109 doForcedMeasurement = pexConfig.Field(
112 doc=
"Force photometer diaSource locations on PVI?")
113 doAddMetrics = pexConfig.Field(
116 doc=
"Add columns to the source table to hold analysis metrics?"
118 detection = pexConfig.ConfigurableField(
119 target=SourceDetectionTask,
120 doc=
"Final source detection for diaSource measurement",
122 deblend = pexConfig.ConfigurableField(
123 target=lsst.meas.deblender.SourceDeblendTask,
124 doc=
"Task to split blended sources into their components."
126 measurement = pexConfig.ConfigurableField(
127 target=DipoleFitTask,
128 doc=
"Task to measure sources on the difference image.",
130 doApCorr = lsst.pex.config.Field(
133 doc=
"Run subtask to apply aperture corrections"
135 applyApCorr = lsst.pex.config.ConfigurableField(
136 target=ApplyApCorrTask,
137 doc=
"Task to apply aperture corrections"
139 forcedMeasurement = pexConfig.ConfigurableField(
140 target=ForcedMeasurementTask,
141 doc=
"Task to force photometer science image at diaSource locations.",
143 growFootprint = pexConfig.Field(
146 doc=
"Grow positive and negative footprints by this many pixels before merging"
148 diaSourceMatchRadius = pexConfig.Field(
151 doc=
"Match radius (in arcseconds) for DiaSource to Source association"
153 doSkySources = pexConfig.Field(
156 doc=
"Generate sky sources?",
158 skySources = pexConfig.ConfigurableField(
159 target=SkyObjectsTask,
160 doc=
"Generate sky sources",
162 setPrimaryFlags = pexConfig.ConfigurableField(
163 target=SetPrimaryFlagsTask,
164 doc=
"Task to add isPrimary and deblending-related flags to the catalog."
166 badSourceFlags = lsst.pex.config.ListField(
168 doc=
"Sources with any of these flags set are removed before writing the output catalog.",
169 default=(
"base_PixelFlags_flag_offimage",
170 "base_PixelFlags_flag_interpolatedCenterAll",
171 "base_PixelFlags_flag_badCenterAll",
172 "base_PixelFlags_flag_edgeCenterAll",
173 "base_PixelFlags_flag_saturatedCenterAll",
176 metricsMaskPlanes = lsst.pex.config.ListField(
178 doc=
"List of mask planes to include in metrics",
179 default=(
'BAD',
'CLIPPED',
'CR',
'DETECTED',
'DETECTED_NEGATIVE',
'EDGE',
180 'INEXACT_PSF',
'INJECTED',
'INJECTED_TEMPLATE',
'INTRP',
'NOT_DEBLENDED',
181 'NO_DATA',
'REJECTED',
'SAT',
'SAT_TEMPLATE',
'SENSOR_EDGE',
'STREAK',
'SUSPECT',
185 metricSources = pexConfig.ConfigurableField(
186 target=SkyObjectsTask,
187 doc=
"Generate QA metric sources",
189 doWriteMetrics = lsst.pex.config.Field(
192 doc=
"Compute and write summary metrics."
194 idGenerator = DetectorVisitIdGeneratorConfig.make_field()
196 def setDefaults(self):
198 self.detection.thresholdPolarity =
"both"
199 self.detection.thresholdValue = 5.0
200 self.detection.reEstimateBackground =
False
201 self.detection.thresholdType =
"pixel_stdev"
202 self.detection.excludeMaskPlanes = [
"EDGE"]
205 self.measurement.algorithms.names.add(
"base_PeakLikelihoodFlux")
206 self.measurement.plugins.names |= [
"ext_trailedSources_Naive",
207 "base_LocalPhotoCalib",
209 "ext_shapeHSM_HsmSourceMoments",
210 "ext_shapeHSM_HsmPsfMoments",
212 self.measurement.slots.psfShape =
"ext_shapeHSM_HsmPsfMoments"
213 self.measurement.slots.shape =
"ext_shapeHSM_HsmSourceMoments"
214 self.measurement.plugins[
"base_NaiveCentroid"].maxDistToPeak = 5.0
215 self.measurement.plugins[
"base_SdssCentroid"].maxDistToPeak = 5.0
216 self.forcedMeasurement.plugins = [
"base_TransformedCentroid",
"base_PsfFlux"]
217 self.forcedMeasurement.copyColumns = {
218 "id":
"objectId",
"parent":
"parentObjectId",
"coord_ra":
"coord_ra",
"coord_dec":
"coord_dec"}
219 self.forcedMeasurement.slots.centroid =
"base_TransformedCentroid"
220 self.forcedMeasurement.slots.shape =
None
223 self.measurement.plugins[
"base_PixelFlags"].masksFpAnywhere = [
224 "STREAK",
"INJECTED",
"INJECTED_TEMPLATE"]
225 self.measurement.plugins[
"base_PixelFlags"].masksFpCenter = [
226 "STREAK",
"INJECTED",
"INJECTED_TEMPLATE"]
227 self.skySources.avoidMask = [
"DETECTED",
"DETECTED_NEGATIVE",
"BAD",
"NO_DATA",
"EDGE"]
228 self.metricSources.avoidMask = [
"NO_DATA",
"EDGE"]
232 """Detect and measure sources on a difference image.
234 ConfigClass = DetectAndMeasureConfig
235 _DefaultName =
"detectAndMeasure"
237 def __init__(self, **kwargs):
238 super().__init__(**kwargs)
239 self.schema = afwTable.SourceTable.makeMinimalSchema()
241 afwTable.CoordKey.addErrorFields(self.schema)
244 self.makeSubtask(
"detection", schema=self.schema)
245 self.makeSubtask(
"deblend", schema=self.schema)
246 self.makeSubtask(
"setPrimaryFlags", schema=self.schema, isSingleFrame=
True)
247 self.makeSubtask(
"measurement", schema=self.schema,
248 algMetadata=self.algMetadata)
249 if self.config.doApCorr:
250 self.makeSubtask(
"applyApCorr", schema=self.measurement.schema)
251 if self.config.doForcedMeasurement:
252 self.schema.addField(
253 "ip_diffim_forced_PsfFlux_instFlux",
"D",
254 "Forced PSF flux measured on the direct image.",
256 self.schema.addField(
257 "ip_diffim_forced_PsfFlux_instFluxErr",
"D",
258 "Forced PSF flux error measured on the direct image.",
260 self.schema.addField(
261 "ip_diffim_forced_PsfFlux_area",
"F",
262 "Forced PSF flux effective area of PSF.",
264 self.schema.addField(
265 "ip_diffim_forced_PsfFlux_flag",
"Flag",
266 "Forced PSF flux general failure flag.")
267 self.schema.addField(
268 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
"Flag",
269 "Forced PSF flux not enough non-rejected pixels in data to attempt the fit.")
270 self.schema.addField(
271 "ip_diffim_forced_PsfFlux_flag_edge",
"Flag",
272 "Forced PSF flux object was too close to the edge of the image to use the full PSF model.")
273 self.makeSubtask(
"forcedMeasurement", refSchema=self.schema)
275 self.schema.addField(
"refMatchId",
"L",
"unique id of reference catalog match")
276 self.schema.addField(
"srcMatchId",
"L",
"unique id of source match")
277 if self.config.doSkySources:
278 self.makeSubtask(
"skySources", schema=self.schema)
281 for flag
in self.config.badSourceFlags:
282 if flag
not in self.schema:
283 raise pipeBase.InvalidQuantumError(
"Field %s not in schema" % flag)
285 if self.config.doWriteMetrics:
286 self.makeSubtask(
"metricSources")
287 self.metricSchema = afwTable.SourceTable.makeMinimalSchema()
288 self.metricSchema.addField(
290 "X location of the metric evaluation.",
292 self.metricSchema.addField(
294 "Y location of the metric evaluation.",
296 self.metricSources.skySourceKey = self.metricSchema.addField(
"sky_source", type=
"Flag",
297 doc=
"Metric evaluation objects.")
298 self.metricSchema.addField(
299 "source_density",
"F",
300 "Density of diaSources at location.",
301 units=
"count/degree^2")
302 self.metricSchema.addField(
303 "dipole_density",
"F",
304 "Density of dipoles at location.",
305 units=
"count/degree^2")
306 self.metricSchema.addField(
307 "dipole_direction",
"F",
308 "Mean dipole orientation.",
310 self.metricSchema.addField(
311 "dipole_separation",
"F",
312 "Mean dipole separation.",
314 self.metricSchema.addField(
315 "template_value",
"F",
316 "Median of template at location.",
318 self.metricSchema.addField(
319 "science_value",
"F",
320 "Median of science at location.",
322 self.metricSchema.addField(
324 "Median of diffim at location.",
326 self.metricSchema.addField(
327 "science_psfSize",
"F",
328 "Width of the science image PSF at location.",
330 self.metricSchema.addField(
331 "template_psfSize",
"F",
332 "Width of the template image PSF at location.",
334 for maskPlane
in self.config.metricsMaskPlanes:
335 self.metricSchema.addField(
336 "%s_mask_fraction"%maskPlane.lower(),
"F",
337 "Fraction of pixels with %s mask"%maskPlane
341 self.outputSchema = afwTable.SourceCatalog(self.schema)
342 self.outputSchema.getTable().setMetadata(self.algMetadata)
344 def runQuantum(self, butlerQC: pipeBase.QuantumContext,
345 inputRefs: pipeBase.InputQuantizedConnection,
346 outputRefs: pipeBase.OutputQuantizedConnection):
347 inputs = butlerQC.get(inputRefs)
348 idGenerator = self.config.idGenerator.apply(butlerQC.quantum.dataId)
349 idFactory = idGenerator.make_table_id_factory()
350 outputs = self.run(**inputs, idFactory=idFactory)
351 butlerQC.put(outputs, outputRefs)
354 def run(self, science, matchedTemplate, difference,
356 """Detect and measure sources on a difference image.
358 The difference image will be convolved with a gaussian approximation of
359 the PSF to form a maximum likelihood image for detection.
360 Close positive and negative detections will optionally be merged into
362 Sky sources, or forced detections in background regions, will optionally
363 be added, and the configured measurement algorithm will be run on all
368 science : `lsst.afw.image.ExposureF`
369 Science exposure that the template was subtracted from.
370 matchedTemplate : `lsst.afw.image.ExposureF`
371 Warped and PSF-matched template that was used produce the
373 difference : `lsst.afw.image.ExposureF`
374 Result of subtracting template from the science image.
375 idFactory : `lsst.afw.table.IdFactory`, optional
376 Generator object used to assign ids to detected sources in the
377 difference image. Ids from this generator are not set until after
378 deblending and merging positive/negative peaks.
382 measurementResults : `lsst.pipe.base.Struct`
384 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
385 Subtracted exposure with detection mask applied.
386 ``diaSources`` : `lsst.afw.table.SourceCatalog`
387 The catalog of detected sources.
389 if idFactory
is None:
390 idFactory = lsst.meas.base.IdGenerator().make_table_id_factory()
393 mask = difference.mask
394 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
399 table = afwTable.SourceTable.make(self.schema)
400 results = self.detection.
run(
406 sources, positives, negatives = self._deblend(difference,
410 return self.processResults(science, matchedTemplate, difference, sources, idFactory,
411 positiveFootprints=positives,
412 negativeFootprints=negatives)
414 def processResults(self, science, matchedTemplate, difference, sources, idFactory,
415 positiveFootprints=None, negativeFootprints=None,):
416 """Measure and process the results of source detection.
420 science : `lsst.afw.image.ExposureF`
421 Science exposure that the template was subtracted from.
422 matchedTemplate : `lsst.afw.image.ExposureF`
423 Warped and PSF-matched template that was used produce the
425 difference : `lsst.afw.image.ExposureF`
426 Result of subtracting template from the science image.
427 sources : `lsst.afw.table.SourceCatalog`
428 Detected sources on the difference exposure.
429 idFactory : `lsst.afw.table.IdFactory`
430 Generator object used to assign ids to detected sources in the
432 positiveFootprints : `lsst.afw.detection.FootprintSet`, optional
433 Positive polarity footprints.
434 negativeFootprints : `lsst.afw.detection.FootprintSet`, optional
435 Negative polarity footprints.
439 measurementResults : `lsst.pipe.base.Struct`
441 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
442 Subtracted exposure with detection mask applied.
443 ``diaSources`` : `lsst.afw.table.SourceCatalog`
444 The catalog of detected sources.
446 self.metadata.add(
"nUnmergedDiaSources",
len(sources))
447 if self.config.doMerge:
448 fpSet = positiveFootprints
449 fpSet.merge(negativeFootprints, self.config.growFootprint,
450 self.config.growFootprint,
False)
451 initialDiaSources = afwTable.SourceCatalog(self.schema)
452 fpSet.makeSources(initialDiaSources)
453 self.log.
info(
"Merging detections into %d sources",
len(initialDiaSources))
455 initialDiaSources = sources
459 for source
in initialDiaSources:
462 initialDiaSources.getTable().setIdFactory(idFactory)
463 initialDiaSources.setMetadata(self.algMetadata)
465 self.metadata.add(
"nMergedDiaSources",
len(initialDiaSources))
467 if self.config.doSkySources:
468 self.addSkySources(initialDiaSources, difference.mask, difference.info.id)
470 if not initialDiaSources.isContiguous():
471 initialDiaSources = initialDiaSources.copy(deep=
True)
473 self.measureDiaSources(initialDiaSources, science, difference, matchedTemplate)
474 diaSources = self._removeBadSources(initialDiaSources)
476 if self.config.doForcedMeasurement:
477 self.measureForcedSources(diaSources, science, difference.getWcs())
479 spatiallySampledMetrics = self.calculateMetrics(difference, diaSources, science, matchedTemplate,
482 measurementResults = pipeBase.Struct(
483 subtractedMeasuredExposure=difference,
484 diaSources=diaSources,
485 spatiallySampledMetrics=spatiallySampledMetrics,
488 return measurementResults
490 def _deblend(self, difference, positiveFootprints, negativeFootprints):
491 """Deblend the positive and negative footprints and return a catalog
492 containing just the children, and the deblended footprints.
496 difference : `lsst.afw.image.Exposure`
497 Result of subtracting template from the science image.
498 positiveFootprints, negativeFootprints : `lsst.afw.detection.FootprintSet`
499 Positive and negative polarity footprints measured on
500 ``difference`` to be deblended separately.
504 sources : `lsst.afw.table.SourceCatalog`
505 Positive and negative deblended children.
506 positives, negatives : `lsst.afw.detection.FootprintSet`
507 Deblended positive and negative polarity footprints measured on
511 footprints = afwDetection.FootprintSet(difference.getBBox())
512 footprints.setFootprints([src.getFootprint()
for src
in sources])
516 """Deblend a positive or negative footprint set,
517 and return the deblended children.
519 sources = afwTable.SourceCatalog(self.schema)
520 footprints.makeSources(sources)
521 self.deblend.
run(exposure=difference, sources=sources)
522 self.setPrimaryFlags.
run(sources)
523 children = sources[
"detect_isDeblendedSource"] == 1
524 sources = sources[children].copy(deep=
True)
526 sources[
'parent'] = 0
527 return sources.copy(deep=
True)
529 positives =
deblend(positiveFootprints)
530 negatives =
deblend(negativeFootprints)
532 sources = afwTable.SourceCatalog(self.schema)
533 sources.reserve(
len(positives) +
len(negatives))
534 sources.extend(positives, deep=
True)
535 sources.extend(negatives, deep=
True)
539 """Remove unphysical diaSources from the catalog.
543 diaSources : `lsst.afw.table.SourceCatalog`
544 The catalog of detected sources.
548 diaSources : `lsst.afw.table.SourceCatalog`
549 The updated catalog of detected sources, with any source that has a
550 flag in ``config.badSourceFlags`` set removed.
552 selector = np.ones(
len(diaSources), dtype=bool)
553 for flag
in self.config.badSourceFlags:
554 flags = diaSources[flag]
555 nBad = np.count_nonzero(flags)
557 self.log.debug(
"Found %d unphysical sources with flag %s.", nBad, flag)
559 nBadTotal = np.count_nonzero(~selector)
560 self.metadata.add(
"nRemovedBadFlaggedSources", nBadTotal)
561 self.log.
info(
"Removed %d unphysical sources.", nBadTotal)
562 return diaSources[selector].copy(deep=
True)
566 """Add sources in empty regions of the difference image
567 for measuring the background.
571 diaSources : `lsst.afw.table.SourceCatalog`
572 The catalog of detected sources.
573 mask : `lsst.afw.image.Mask`
574 Mask plane for determining regions where Sky sources can be added.
576 Seed value to initialize the random number generator.
579 subtask = self.skySources
580 skySourceFootprints = subtask.run(mask=mask, seed=seed, catalog=diaSources)
581 self.metadata.add(f
"n_{subtask.getName()}",
len(skySourceFootprints))
584 """Use (matched) template and science image to constrain dipole fitting.
588 diaSources : `lsst.afw.table.SourceCatalog`
589 The catalog of detected sources.
590 science : `lsst.afw.image.ExposureF`
591 Science exposure that the template was subtracted from.
592 difference : `lsst.afw.image.ExposureF`
593 Result of subtracting template from the science image.
594 matchedTemplate : `lsst.afw.image.ExposureF`
595 Warped and PSF-matched template that was used produce the
599 for mp
in self.config.measurement.plugins[
"base_PixelFlags"].masksFpAnywhere:
600 difference.mask.addMaskPlane(mp)
603 self.measurement.
run(diaSources, difference, science, matchedTemplate)
604 if self.config.doApCorr:
605 apCorrMap = difference.getInfo().getApCorrMap()
606 if apCorrMap
is None:
607 self.log.
warning(
"Difference image does not have valid aperture correction; skipping.")
609 self.applyApCorr.
run(
615 """Perform forced measurement of the diaSources on the science image.
619 diaSources : `lsst.afw.table.SourceCatalog`
620 The catalog of detected sources.
621 science : `lsst.afw.image.ExposureF`
622 Science exposure that the template was subtracted from.
623 wcs : `lsst.afw.geom.SkyWcs`
624 Coordinate system definition (wcs) for the exposure.
628 forcedSources = self.forcedMeasurement.generateMeasCat(science, diaSources, wcs)
629 self.forcedMeasurement.
run(forcedSources, science, diaSources, wcs)
630 mapper = afwTable.SchemaMapper(forcedSources.schema, diaSources.schema)
631 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFlux")[0],
632 "ip_diffim_forced_PsfFlux_instFlux",
True)
633 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFluxErr")[0],
634 "ip_diffim_forced_PsfFlux_instFluxErr",
True)
635 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_area")[0],
636 "ip_diffim_forced_PsfFlux_area",
True)
637 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag")[0],
638 "ip_diffim_forced_PsfFlux_flag",
True)
639 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_noGoodPixels")[0],
640 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
True)
641 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_edge")[0],
642 "ip_diffim_forced_PsfFlux_flag_edge",
True)
643 for diaSource, forcedSource
in zip(diaSources, forcedSources):
644 diaSource.assign(forcedSource, mapper)
646 def calculateMetrics(self, difference, diaSources, science, matchedTemplate, idFactory):
647 """Add image QA metrics to the Task metadata.
651 difference : `lsst.afw.image.Exposure`
652 The target image to calculate metrics for.
653 diaSources : `lsst.afw.table.SourceCatalog`
654 The catalog of detected sources.
655 science : `lsst.afw.image.Exposure`
657 matchedTemplate : `lsst.afw.image.Exposure`
658 The reference image, warped and psf-matched to the science image.
659 idFactory : `lsst.afw.table.IdFactory`, optional
660 Generator object used to assign ids to detected sources in the
665 spatiallySampledMetrics : `lsst.afw.table.SourceCatalog`, or `None`
666 A catalog of randomized locations containing locally evaluated
669 mask = difference.mask
670 badPix = (mask.array & mask.getPlaneBitMask(self.config.detection.excludeMaskPlanes)) > 0
671 self.metadata.add(
"nGoodPixels", np.sum(~badPix))
672 self.metadata.add(
"nBadPixels", np.sum(badPix))
673 detPosPix = (mask.array & mask.getPlaneBitMask(
"DETECTED")) > 0
674 detNegPix = (mask.array & mask.getPlaneBitMask(
"DETECTED_NEGATIVE")) > 0
675 self.metadata.add(
"nPixelsDetectedPositive", np.sum(detPosPix))
676 self.metadata.add(
"nPixelsDetectedNegative", np.sum(detNegPix))
679 self.metadata.add(
"nBadPixelsDetectedPositive", np.sum(detPosPix))
680 self.metadata.add(
"nBadPixelsDetectedNegative", np.sum(detNegPix))
681 metricsMaskPlanes = []
682 for maskPlane
in self.config.metricsMaskPlanes:
685 metricsMaskPlanes.append(maskPlane)
686 except InvalidParameterError:
687 self.metadata.add(
"%s_mask_fraction"%maskPlane.lower(), -1)
688 self.log.
info(
"Unable to calculate metrics for mask plane %s: not in image"%maskPlane)
690 if self.config.doWriteMetrics:
691 spatiallySampledMetrics = afwTable.SourceCatalog(self.metricSchema)
692 spatiallySampledMetrics.getTable().setIdFactory(idFactory)
693 self.addSkySources(spatiallySampledMetrics, science.mask, difference.info.id,
694 subtask=self.metricSources)
695 for src
in spatiallySampledMetrics:
696 self._evaluateLocalMetric(src, diaSources, science, matchedTemplate, difference,
697 metricsMaskPlanes=metricsMaskPlanes)
699 return spatiallySampledMetrics.asAstropy()
703 """Calculate image quality metrics at spatially sampled locations.
707 src : `lsst.afw.table.SourceRecord`
708 The source record to be updated with metric calculations.
709 diaSources : `lsst.afw.table.SourceCatalog`
710 The catalog of detected sources.
711 science : `lsst.afw.image.Exposure`
713 matchedTemplate : `lsst.afw.image.Exposure`
714 The reference image, warped and psf-matched to the science image.
715 difference : `lsst.afw.image.Exposure`
716 Result of subtracting template from the science image.
717 metricsMaskPlanes : `list` of `str`
718 Mask planes to calculate metrics from.
720 bbox = src.getFootprint().getBBox()
721 pix = bbox.getCenter()
722 src.set(
'science_psfSize', getPsfFwhm(science.psf, position=pix))
723 src.set(
'template_psfSize', getPsfFwhm(matchedTemplate.psf, position=pix))
725 metricRegionSize = 100
726 bbox.grow(metricRegionSize)
727 bbox = bbox.clippedTo(science.getBBox())
728 nPix = bbox.getArea()
729 pixScale = science.wcs.getPixelScale()
730 area = nPix*pixScale.asDegrees()**2
731 peak = src.getFootprint().getPeaks()[0]
732 src.set(
'x', peak[
'i_x'])
733 src.set(
'y', peak[
'i_y'])
734 src.setCoord(science.wcs.pixelToSky(peak[
'i_x'], peak[
'i_y']))
735 selectSources = diaSources[bbox.contains(diaSources.getX(), diaSources.getY())]
736 if self.config.doSkySources:
737 selectSources = selectSources[~selectSources[
"sky_source"]]
738 sourceDensity =
len(selectSources)/area
739 dipoleSources = selectSources[selectSources[
"ip_diffim_DipoleFit_flag_classification"]]
740 dipoleDensity =
len(dipoleSources)/area
742 meanDipoleOrientation = angleMean(dipoleSources[
"ip_diffim_DipoleFit_orientation"])
743 src.set(
'dipole_direction', meanDipoleOrientation)
744 meanDipoleSeparation = np.mean(dipoleSources[
"ip_diffim_DipoleFit_separation"])
745 src.set(
'dipole_separation', meanDipoleSeparation)
746 templateVal = np.median(matchedTemplate[bbox].image.array)
747 scienceVal = np.median(science[bbox].image.array)
748 diffimVal = np.median(difference[bbox].image.array)
749 src.set(
'source_density', sourceDensity)
750 src.set(
'dipole_density', dipoleDensity)
751 src.set(
'template_value', templateVal)
752 src.set(
'science_value', scienceVal)
753 src.set(
'diffim_value', diffimVal)
754 for maskPlane
in metricsMaskPlanes:
755 src.set(
"%s_mask_fraction"%maskPlane.lower(),
761 scoreExposure = pipeBase.connectionTypes.Input(
762 doc=
"Maximum likelihood image for detection.",
763 dimensions=(
"instrument",
"visit",
"detector"),
764 storageClass=
"ExposureF",
765 name=
"{fakesType}{coaddName}Diff_scoreExp",
770 pipelineConnections=DetectAndMeasureScoreConnections):
775 """Detect DIA sources using a score image,
776 and measure the detections on the difference image.
778 Source detection is run on the supplied score, or maximum likelihood,
779 image. Note that no additional convolution will be done in this case.
780 Close positive and negative detections will optionally be merged into
782 Sky sources, or forced detections in background regions, will optionally
783 be added, and the configured measurement algorithm will be run on all
786 ConfigClass = DetectAndMeasureScoreConfig
787 _DefaultName =
"detectAndMeasureScore"
790 def run(self, science, matchedTemplate, difference, scoreExposure,
792 """Detect and measure sources on a score image.
796 science : `lsst.afw.image.ExposureF`
797 Science exposure that the template was subtracted from.
798 matchedTemplate : `lsst.afw.image.ExposureF`
799 Warped and PSF-matched template that was used produce the
801 difference : `lsst.afw.image.ExposureF`
802 Result of subtracting template from the science image.
803 scoreExposure : `lsst.afw.image.ExposureF`
804 Score or maximum likelihood difference image
805 idFactory : `lsst.afw.table.IdFactory`, optional
806 Generator object used to assign ids to detected sources in the
807 difference image. Ids from this generator are not set until after
808 deblending and merging positive/negative peaks.
812 measurementResults : `lsst.pipe.base.Struct`
814 ``subtractedMeasuredExposure`` : `lsst.afw.image.ExposureF`
815 Subtracted exposure with detection mask applied.
816 ``diaSources`` : `lsst.afw.table.SourceCatalog`
817 The catalog of detected sources.
819 if idFactory
is None:
820 idFactory = lsst.meas.base.IdGenerator().make_table_id_factory()
823 mask = scoreExposure.mask
824 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
829 table = afwTable.SourceTable.make(self.schema)
830 results = self.detection.
run(
832 exposure=scoreExposure,
836 difference.mask.assign(scoreExposure.mask, scoreExposure.getBBox())
838 sources, positives, negatives = self._deblend(difference,
842 return self.processResults(science, matchedTemplate, difference, sources, idFactory,
843 positiveFootprints=positives, negativeFootprints=negatives)
847 nMaskSet = np.count_nonzero((mask.array & mask.getPlaneBitMask(maskPlane)))
848 return nMaskSet/mask.array.size
Asseses the quality of a candidate given a spatial kernel and background model.
run(self, coaddExposures, bbox, wcs, dataIds, physical_filter=None, **kwargs)