Coverage for python / lsst / drp / tasks / reprocess_visit_image.py: 22%
223 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-05 18:49 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-05 18:49 +0000
1# This file is part of drp_tasks.
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/>.
22__all__ = ["ReprocessVisitImageTask", "ReprocessVisitImageConfig", "combine_backgrounds"]
24import numpy as np
25import smatch
27import lsst.afw.image as afwImage
28import lsst.afw.table as afwTable
29import lsst.geom
30import lsst.meas.algorithms
31import lsst.meas.deblender
32import lsst.meas.extensions.photometryKron
33import lsst.meas.extensions.shapeHSM
34import lsst.pex.config as pexConfig
35import lsst.pipe.base as pipeBase
36from lsst.pipe.base import connectionTypes
37from lsst.pipe.tasks import computeExposureSummaryStats, repair, snapCombine
40class ReprocessVisitImageConnections(
41 pipeBase.PipelineTaskConnections, dimensions=("instrument", "visit", "detector")
42):
43 exposures = connectionTypes.Input(
44 doc="Exposure (or two snaps) to be calibrated, and detected and measured on.",
45 name="postISRCCD",
46 storageClass="Exposure",
47 multiple=True, # to handle 1 exposure or 2 snaps
48 dimensions=["instrument", "exposure", "detector"],
49 )
50 preliminary_mask = connectionTypes.Input(
51 doc="Mask plane calculated in the initial calibration step.",
52 name="preliminary_visit_mask",
53 storageClass="Mask",
54 dimensions=["instrument", "visit", "detector"],
55 )
56 visit_summary = connectionTypes.Input(
57 doc="Visit-level catalog summarizing all image characterizations and calibrations.",
58 name="finalVisitSummary",
59 storageClass="ExposureCatalog",
60 dimensions=["instrument", "visit"],
61 )
62 initial_photo_calib = connectionTypes.Input(
63 doc="Photometric calibration that was applied to exposure during the measurement of background_1."
64 " Used to uncalibrate the background before subtracting it from the input exposure.",
65 name="initial_photoCalib_detector",
66 storageClass="PhotoCalib",
67 dimensions=("instrument", "visit", "detector"),
68 )
69 background_1 = connectionTypes.Input(
70 doc="Background models estimated during calibration.",
71 name="initial_pvi_background",
72 storageClass="Background",
73 dimensions=("instrument", "visit", "detector"),
74 )
75 background_2 = connectionTypes.Input(
76 doc="Background that was fit on top of background_1.",
77 name="skyCorr",
78 dimensions=("instrument", "visit", "detector"),
79 storageClass="Background",
80 )
81 calib_sources = connectionTypes.Input(
82 doc="Per-visit catalog of measurements to get 'calib_*' flags from.",
83 name="finalized_src_table",
84 storageClass="ArrowAstropy",
85 dimensions=["instrument", "visit"],
86 )
87 background_to_photometric_ratio = connectionTypes.Input(
88 doc=(
89 "Ratio of a background-flattened image to a photometric-flattened image. "
90 "Only used if do_apply_flat_background_ratio is True."
91 ),
92 name="background_to_photometric_ratio",
93 storageClass="Image",
94 dimensions=("instrument", "visit", "detector"),
95 )
96 # TODO DM-46947: pull in the STREAK mask from CompareWarp.
98 # outputs
99 sources_schema = connectionTypes.InitOutput(
100 doc="Schema of the output sources catalog.",
101 name="sources_schema",
102 storageClass="SourceCatalog",
103 )
105 exposure = connectionTypes.Output(
106 doc="Photometrically calibrated exposure with attached calibrations and summary statistics.",
107 name="pvi",
108 storageClass="ExposureF",
109 dimensions=("instrument", "visit", "detector"),
110 )
111 sources = connectionTypes.Output(
112 doc="Catalog of measured sources detected on the calibrated exposure.",
113 name="sources_detector",
114 storageClass="ArrowAstropy",
115 dimensions=["instrument", "visit", "detector"],
116 )
117 sources_footprints = connectionTypes.Output(
118 doc="Catalog of measured sources detected on the calibrated exposure; includes source footprints.",
119 name="sources_footprints_detector",
120 storageClass="SourceCatalog",
121 dimensions=["instrument", "visit", "detector"],
122 )
123 background = connectionTypes.Output(
124 doc=(
125 "Total background model including new detections in this task. "
126 "Note that the background model has units of ADU, while the corresponding "
127 "image has units of nJy - the image must be 'uncalibrated' before the background "
128 "can be restored."
129 ),
130 name="pvi_background",
131 dimensions=("instrument", "visit", "detector"),
132 storageClass="Background",
133 )
135 def __init__(self, *, config=None):
136 if not config.do_use_sky_corr:
137 del self.background_2
138 if not config.remove_initial_photo_calib:
139 del self.initial_photo_calib
140 if not config.do_apply_flat_background_ratio:
141 del self.background_to_photometric_ratio
144class ReprocessVisitImageConfig(
145 pipeBase.PipelineTaskConfig, pipelineConnections=ReprocessVisitImageConnections
146):
147 # To generate catalog ids consistently across subtasks.
148 id_generator = lsst.meas.base.DetectorVisitIdGeneratorConfig.make_field()
150 do_use_sky_corr = pexConfig.Field(
151 dtype=bool,
152 default=False,
153 doc="Include the skyCorr input for background subtraction?",
154 )
155 remove_initial_photo_calib = pexConfig.Field(
156 dtype=bool,
157 default=False,
158 doc="Remove an already-applied photometric calibration from the backgrounds?",
159 )
160 snap_combine = pexConfig.ConfigurableField(
161 target=snapCombine.SnapCombineTask,
162 doc="Task to combine two snaps to make one exposure.",
163 )
164 repair = pexConfig.ConfigurableField(
165 target=repair.RepairTask,
166 doc="Task to repair cosmic rays on the exposure before PSF determination.",
167 )
168 detection = pexConfig.ConfigurableField(
169 target=lsst.meas.algorithms.SourceDetectionTask,
170 doc="Task to detect sources to return in the output catalog.",
171 )
172 sky_sources = pexConfig.ConfigurableField(
173 target=lsst.meas.algorithms.SkyObjectsTask,
174 doc="Task to generate sky sources ('empty' regions where there are no detections).",
175 )
176 deblend = pexConfig.ConfigurableField(
177 target=lsst.meas.deblender.SourceDeblendTask, doc="Split blended sources into their components."
178 )
179 measurement = pexConfig.ConfigurableField(
180 target=lsst.meas.base.SingleFrameMeasurementTask,
181 doc="Task to measure sources to return in the output catalog.",
182 )
183 normalized_calibration_flux = pexConfig.ConfigurableField(
184 target=lsst.meas.algorithms.NormalizedCalibrationFluxTask,
185 doc="Task to normalize the calibration flux (e.g. compensated tophats).",
186 )
187 apply_aperture_correction = pexConfig.ConfigurableField(
188 target=lsst.meas.base.ApplyApCorrTask,
189 doc="Task to apply aperture corrections to the measured sources.",
190 )
191 set_primary_flags = pexConfig.ConfigurableField(
192 target=lsst.meas.algorithms.setPrimaryFlags.SetPrimaryFlagsTask,
193 doc="Task to add isPrimary to the catalog.",
194 )
195 catalog_calculation = pexConfig.ConfigurableField(
196 target=lsst.meas.base.CatalogCalculationTask,
197 doc="Task to compute catalog values using only the catalog entries.",
198 )
199 post_calculations = pexConfig.ConfigurableField(
200 target=lsst.meas.base.SingleFrameMeasurementTask,
201 doc="Task to compute catalog values after all other calculations have been done.",
202 )
203 compute_summary_stats = pexConfig.ConfigurableField(
204 target=computeExposureSummaryStats.ComputeExposureSummaryStatsTask,
205 doc="Task to to compute summary statistics on the calibrated exposure.",
206 )
207 calib_match_radius = pexConfig.Field(
208 dtype=float,
209 default=0.2,
210 doc="Radius in arcseconds to cross-match calib_sources to the output catalog.",
211 )
212 do_apply_flat_background_ratio = pexConfig.Field(
213 dtype=bool,
214 default=False,
215 doc="This should be True if processing was done with an illumination correction.",
216 )
217 copyMaskPlanes = lsst.pex.config.ListField(
218 dtype=str, default=("SPIKE",), doc="Mask planes to copy from the initial calibration task."
219 )
221 def setDefaults(self):
222 super().setDefaults()
224 # No need to redo background: we have the global background model.
225 self.detection.reEstimateBackground = False
226 self.detection.doTempLocalBackground = False
228 # NOTE: these apertures were selected for HSC, and may not be
229 # what we want for LSSTCam.
230 self.measurement.plugins["base_CircularApertureFlux"].radii = [
231 3.0,
232 4.5,
233 6.0,
234 9.0,
235 12.0,
236 17.0,
237 25.0,
238 35.0,
239 50.0,
240 70.0,
241 ]
242 lsst.meas.extensions.shapeHSM.configure_hsm(self.measurement)
243 self.measurement.plugins.names |= ["base_Jacobian", "base_FPPosition", "ext_photometryKron_KronFlux"]
244 self.measurement.plugins["base_Jacobian"].pixelScale = 0.2
246 # TODO DM-46306: should make this the ApertureFlux default!
247 # Use a large aperture to be independent of seeing in calibration
248 self.measurement.plugins["base_CircularApertureFlux"].maxSincRadius = 12.0
250 # Only apply calibration fluxes, do not measure them.
251 self.normalized_calibration_flux.do_measure_ap_corr = False
253 self.post_calculations.plugins.names = ["base_LocalPhotoCalib", "base_LocalWcs"]
254 self.post_calculations.doReplaceWithNoise = False
255 for key in self.post_calculations.slots:
256 setattr(self.post_calculations.slots, key, None)
258 def validate(self):
259 super().validate()
261 if self.do_apply_flat_background_ratio:
262 if self.detection.reEstimateBackground:
263 if not self.detection.doApplyFlatBackgroundRatio:
264 raise pexConfig.FieldValidationError(
265 ReprocessVisitImageConfig.detection,
266 self,
267 "ReprocessVisitImageConfig.detection background must be configured with "
268 "doApplyFlatBackgroundRatio if do_apply_flat_background_ratio is True.",
269 )
272class ReprocessVisitImageTask(pipeBase.PipelineTask):
273 """Use the visit-level calibrations to perform detection and measurement
274 on the single frame exposures and produce a "final" exposure and catalog.
275 """
277 ConfigClass = ReprocessVisitImageConfig
278 _DefaultName = "reprocessVisitImage"
280 def __init__(self, schema=None, **kwargs):
281 super().__init__(**kwargs)
283 if schema is None:
284 schema = afwTable.SourceTable.makeMinimalSchema()
286 self.makeSubtask("snap_combine")
287 self.makeSubtask("repair")
288 self.makeSubtask("detection", schema=schema)
289 self.makeSubtask("sky_sources", schema=schema)
290 self.makeSubtask("deblend", schema=schema)
291 self.makeSubtask("measurement", schema=schema)
292 self.makeSubtask("normalized_calibration_flux", schema=schema)
293 self.makeSubtask("apply_aperture_correction", schema=schema)
294 self.makeSubtask("catalog_calculation", schema=schema)
295 self.makeSubtask("set_primary_flags", schema=schema, isSingleFrame=True)
296 self.makeSubtask("post_calculations", schema=schema)
297 self.makeSubtask("compute_summary_stats")
299 schema.addField(
300 "visit",
301 type="L",
302 doc="Visit this source appeared on.",
303 )
304 schema.addField(
305 "detector",
306 type="U",
307 doc="Detector this source appeared on.",
308 )
310 # These fields will be propagated from finalizeCharacterization.
311 # It might be better to get them from the finalized catalog instead
312 # (if it output a schema), so the docstrings exactly match.
313 schema.addField(
314 "calib_psf_candidate",
315 type="Flag",
316 doc="Set if the source was a candidate for PSF determination, "
317 "as determined from FinalizeCharacterizationTask.",
318 )
319 schema.addField(
320 "calib_psf_reserved",
321 type="Flag",
322 doc="set if source was reserved from PSF determination by FinalizeCharacterizationTask.",
323 )
324 schema.addField(
325 "calib_psf_used",
326 type="Flag",
327 doc="Set if source was used in the PSF determination by FinalizeCharacterizationTask.",
328 )
329 self.psf_fields = ("calib_psf_candidate", "calib_psf_used", "calib_psf_reserved")
331 # TODO (DM-46971):
332 # These fields are only here to satisfy the SDM schema, and will
333 # be removed from there as they are misleading (because we don't
334 # propagate this information from gbdes/fgcmcal).
335 schema.addField(
336 "calib_photometry_used",
337 type="Flag",
338 doc="Unused; placeholder for SDM schemas.",
339 )
340 schema.addField(
341 "calib_photometry_reserved",
342 type="Flag",
343 doc="Unused; placeholder for SDM schemas.",
344 )
345 schema.addField(
346 "calib_astrometry_used",
347 type="Flag",
348 doc="Unused; placeholder for SDM schemas.",
349 )
350 schema.addField(
351 "calib_astrometry_reserved",
352 type="Flag",
353 doc="Unused; placeholder for SDM schemas.",
354 )
355 # This pre-calibration schema is the one that most methods should use.
356 self.schema = schema
357 # The final catalog will have calibrated flux columns, which we add to
358 # the init-output schema by calibrating our zero-length catalog with an
359 # arbitrary dummy PhotoCalib.
360 dummy_photo_calib = afwImage.PhotoCalib(1.0, 0, bbox=lsst.geom.Box2I())
361 self.sources_schema = dummy_photo_calib.calibrateCatalog(afwTable.SourceCatalog(schema))
363 def runQuantum(self, butlerQC, inputRefs, outputRefs):
364 inputs = butlerQC.get(inputRefs)
365 id_generator = self.config.id_generator.apply(butlerQC.quantum.dataId)
367 detector = outputRefs.exposure.dataId["detector"]
368 exposures = inputs.pop("exposures")
369 preliminary_mask = inputs.pop("preliminary_mask")
370 visit_summary = inputs.pop("visit_summary")
371 calib_sources = inputs.pop("calib_sources")
372 if self.config.remove_initial_photo_calib:
373 initial_photo_calib = inputs.pop("initial_photo_calib")
374 else:
375 initial_photo_calib = None
376 background_1 = inputs.pop("background_1")
377 if self.config.do_use_sky_corr:
378 background_2 = inputs.pop("background_2")
379 background = combine_backgrounds(background_1, background_2)
380 else:
381 background = background_1
382 if self.config.do_apply_flat_background_ratio:
383 background_to_photometric_ratio = inputs.pop("background_to_photometric_ratio")
384 else:
385 background_to_photometric_ratio = None
387 # This should not happen with a properly configured execution context.
388 assert not inputs, "runQuantum got more inputs than expected"
390 detector_summary = visit_summary.find(detector)
391 lines = []
392 if detector_summary is None:
393 lines.append(" > no entry for the detector was found in the visit summary table")
394 else:
395 if detector_summary.psf is None:
396 lines.append(" > the PSF model for the detector is None")
397 if detector_summary.wcs is None:
398 lines.append(" > the WCS model for the detector is None")
399 if detector_summary.apCorrMap is None:
400 lines.append(" > the aperture correction model map for the detector is None")
401 if detector_summary.photoCalib is None:
402 lines.append(" > the photometric calibration model for the detector is None")
404 if lines:
405 msg = "\n".join(lines)
406 raise pipeBase.UpstreamFailureNoWorkFound(
407 f"Skipping reprocessing of detector {detector} because:\n{msg}"
408 )
410 # Specify the fields that `annotate` needs below, to ensure they
411 # exist, even as None.
412 result = pipeBase.Struct(
413 exposure=None,
414 sources_footprints=None,
415 )
416 try:
417 self.run(
418 exposures=exposures,
419 initial_photo_calib=initial_photo_calib,
420 psf=detector_summary.psf,
421 background=background,
422 ap_corr=detector_summary.apCorrMap,
423 photo_calib=detector_summary.photoCalib,
424 wcs=detector_summary.wcs,
425 calib_sources=calib_sources,
426 result=result,
427 id_generator=id_generator,
428 background_to_photometric_ratio=background_to_photometric_ratio,
429 preliminary_mask=preliminary_mask,
430 )
431 except pipeBase.AlgorithmError as e:
432 error = pipeBase.AnnotatedPartialOutputsError.annotate(
433 e, self, result.exposure, result.sources_footprints, log=self.log
434 )
435 butlerQC.put(result, outputRefs)
436 raise error from e
438 butlerQC.put(result, outputRefs)
440 def run(
441 self,
442 *,
443 exposures,
444 initial_photo_calib,
445 psf,
446 background,
447 ap_corr,
448 photo_calib,
449 wcs,
450 calib_sources,
451 preliminary_mask=None,
452 id_generator=None,
453 background_to_photometric_ratio=None,
454 result=None,
455 ):
456 """Detect and measure sources on the exposure(s) (snap combined as
457 necessary), and make a "final" Processed Visit Image using all of the
458 supplied metadata, plus a catalog measured on it.
460 Parameters
461 ----------
462 exposures : `lsst.afw.image.Exposure` or
463 `list` [`lsst.afw.image.Exposure`]
464 Post-ISR exposure(s), with an initial WCS, VisitInfo, and Filter.
465 Modified in-place during processing if only one is passed.
466 If two exposures are passed, treat them as snaps and combine
467 before doing further processing.
468 initial_photo_calib : `lsst.afw.image.PhotoCalib` or `None`
469 Photometric calibration that was applied to exposure during the
470 measurement of the background. Should be `None` if and only if
471 ``config.remove_initial_photo_calib` is false.
472 psf : `lsst.afw.detection.Psf`
473 PSF model for this exposure.
474 background : `lsst.afw.math.BackgroundList`
475 Total background that had been fit to the exposure so far;
476 modified in place to include background fit when detecting sources.
477 ap_corr : `lsst.afw.image.ApCorrMap`
478 Aperture Correction model for this exposure.
479 photo_calib : `lsst.afw.image.PhotoCalib`
480 Photometric calibration model for this exposure.
481 wcs : `lsst.afw.geom.SkyWcs`
482 World Coordinate System model for this exposure.
483 calib_sources : `astropy.table.Table`
484 Per-visit catalog of measurements to get 'calib_*' flags from.
485 preliminary_mask : `lsst.afw.image.Mask`, optional
486 An input Mask to copy individual mask planes from.
487 id_generator : `lsst.meas.base.IdGenerator`, optional
488 Object that generates source IDs and provides random seeds.
489 background_to_photometric_ratio : `lsst.afw.image.ImageF`, optional
490 Background to photometric ratio image, to convert between
491 photometric flattened and background flattened image.
492 result : `lsst.pipe.base.Struct`, optional
493 Result struct that is modified to allow saving of partial outputs
494 for some failure conditions. If the task completes successfully,
495 this is also returned.
497 Returns
498 -------
499 result : `lsst.pipe.base.Struct`
500 Results as a struct with attributes:
502 ``exposure``
503 Calibrated exposure, with pixels in nJy units.
504 (`lsst.afw.image.Exposure`)
505 ``sources``
506 Sources that were measured on the exposure, with calibrated
507 fluxes and magnitudes. (`astropy.table.Table`)
508 ``sources_footprints``
509 Footprints of sources that were measured on the exposure.
510 (`lsst.afw.table.SourceCatalog`)
511 ``background``
512 Total background that was fit to, and subtracted from the
513 exposure when detecting ``sources``, in the same nJy units as
514 ``exposure``. (`lsst.afw.math.BackgroundList`)
515 """
516 if result is None:
517 result = pipeBase.Struct()
518 if id_generator is None:
519 id_generator = lsst.meas.base.IdGenerator()
521 result.exposure = self.snap_combine.run(exposures).exposure
522 if preliminary_mask is not None:
523 self._copyMaskPlanes(result.exposure, preliminary_mask)
525 # Apply the illumination correction if required.
526 # This assumes the input images have had a background-flat applied.
527 if self.config.do_apply_flat_background_ratio:
528 result.exposure.maskedImage /= background_to_photometric_ratio
530 if self.config.remove_initial_photo_calib:
531 # Calibrate the image, so it's on the same units as the background.
532 result.exposure.maskedImage = initial_photo_calib.calibrateImage(result.exposure.maskedImage)
534 with lsst.meas.algorithms.backgroundFlatContext(
535 result.exposure.maskedImage,
536 self.config.do_apply_flat_background_ratio,
537 backgroundToPhotometricRatio=background_to_photometric_ratio,
538 ):
539 result.exposure.maskedImage -= background.getImage()
541 if self.config.remove_initial_photo_calib:
542 # Uncalibrate so that we do the measurements in instFlux, because
543 # we don't have a way to identify measurements as being in nJy.
544 result.exposure.maskedImage /= initial_photo_calib.getCalibrationMean()
546 result.exposure.setPsf(psf)
547 result.exposure.setApCorrMap(ap_corr)
548 result.exposure.setWcs(wcs)
549 result.exposure.setPhotoCalib(photo_calib)
551 result.sources_footprints = self._find_sources(
552 result.exposure,
553 background,
554 calib_sources,
555 id_generator,
556 background_to_photometric_ratio=background_to_photometric_ratio,
557 )
558 result.background = background
559 # TODO (DM-46971):
560 # Now that we're running them before we apply the PhotoCalib to the
561 # image pixels, there's no need for post_calibrations to exist as
562 # a separate measurement instance from the main one (which is invoked
563 # in _find_sources), but it's better to save removal (which may need
564 # to involve a deprecation) until after we've got everything running.
565 self.post_calculations.run(result.sources_footprints, result.exposure)
566 result.exposure.info.setSummaryStats(
567 self.compute_summary_stats.run(result.exposure, result.sources_footprints, background)
568 )
569 result.sources_footprints = self._apply_photo_calib(
570 result.exposure,
571 result.sources_footprints,
572 photo_calib,
573 )
574 result.sources = result.sources_footprints.asAstropy()
576 return result
578 def _copyMaskPlanes(self, exposure, mask):
579 """Copy mask planes from an input Mask to the final Exposure.
581 Parameters
582 ----------
583 exposure : `lsst.afw.image.Exposure`
584 Calibrated exposure; will be modified in place.
585 mask : `lsst.afw.image.Mask`
586 Mask to copy from.
587 """
588 copyMaskPlanes = []
589 for mp in self.config.copyMaskPlanes:
590 if mp in mask.getMaskPlaneDict().keys():
591 copyMaskPlanes.append(mp)
592 bitMask = mask.getPlaneBitMask(copyMaskPlanes)
593 exposure.mask.array |= mask.array & bitMask
595 def _find_sources(
596 self,
597 exposure,
598 background,
599 calib_sources,
600 id_generator,
601 background_to_photometric_ratio=None,
602 ):
603 """Detect and measure sources on the exposure.
605 Parameters
606 ----------
607 exposure : `lsst.afw.image.Exposure`
608 Exposure to detect and measure sources on; must have a valid PSF.
609 background : `lsst.afw.math.BackgroundList`
610 Background that was fit to the exposure during detection;
611 modified in-place during subsequent detection.
612 calib_sources : `astropy.table.Table`
613 Per-visit catalog of measurements to get 'calib_*' flags from.
614 id_generator : `lsst.meas.base.IdGenerator`
615 Object that generates source IDs and provides random seeds.
616 background_to_photometric_ratio : `lsst.afw.image.Image`, optional
617 Image to convert photometric-flattened image to
618 background-flattened image.
620 Returns
621 -------
622 sources
623 Catalog that was detected and measured on the exposure.
624 """
625 table = afwTable.SourceTable.make(self.schema, id_generator.make_table_id_factory())
627 self.repair.run(exposure=exposure)
628 detections = self.detection.run(
629 table=table,
630 exposure=exposure,
631 background=background,
632 backgroundToPhotometricRatio=background_to_photometric_ratio,
633 )
634 sources = detections.sources
636 self.sky_sources.run(exposure.mask, id_generator.catalog_id, sources)
638 self.deblend.run(exposure=exposure, sources=sources)
639 # The deblender may not produce a contiguous catalog; ensure
640 # contiguity for subsequent tasks.
641 if not sources.isContiguous():
642 sources = sources.copy(deep=True)
644 self.measurement.run(sources, exposure)
645 self.normalized_calibration_flux.run(exposure=exposure, catalog=sources)
646 self.apply_aperture_correction.run(sources, exposure.apCorrMap)
647 self.catalog_calculation.run(sources)
648 self.set_primary_flags.run(sources)
650 sources["visit"] = exposure.visitInfo.id
651 sources["detector"] = exposure.info.getDetector().getId()
653 self._match_calib_sources(sources, calib_sources, exposure.info.getDetector().getId())
655 return sources
657 def _match_calib_sources(self, sources, calib_sources, detector):
658 """Match with calib_sources to set `calib_*` flags in the output
659 catalog.
661 Parameters
662 ----------
663 sources : `lsst.afw.table.SourceCatalog`
664 Catalog that was detected and measured on the exposure. Modified
665 in place to set the psf_fields.
666 calib_sources : `astropy.table.Table`
667 Per-visit catalog of measurements to get 'calib_*' flags from.
668 detector : `int`
669 Id of detector for this exposure, to get the correct sources from
670 calib_sources for cross-matching.
671 """
672 # NOTE: we don't remove the sky sources here, but they should be very
673 # far from any actual source, and so should not match with anything.
674 use = calib_sources["detector"] == detector
675 with smatch.Matcher(sources["coord_ra"], sources["coord_dec"]) as matcher:
676 _, i1, i2, _ = matcher.query_knn(
677 calib_sources[use]["coord_ra"],
678 calib_sources[use]["coord_dec"],
679 1,
680 self.config.calib_match_radius / 3600.0,
681 return_indices=True,
682 )
684 for field in self.psf_fields:
685 # NOTE: Have to fill a full-sized array first, then set with it.
686 result = np.zeros(len(sources), dtype=bool)
687 result[i1] = calib_sources[use][i2][field]
688 sources[field] = result
690 def _apply_photo_calib(self, exposure, sources_footprints, photo_calib):
691 """Photometrically calibrate the exposure and catalog with the
692 supplied PhotoCalib, and set the exposure's PhotoCalib to 1.
694 Parameters
695 ----------
696 exposures : `lsst.afw.image.Exposure`
697 Exposure to calibrate and set PhotoCalib on; Modified in place.
698 sources_footprints : `lsst.afw.table.SourceCatalog`
699 Catalog to calibrate.
700 photo_calib : `lsst.afw.image.PhotoCalib`
701 Photometric calibration to apply.
702 calibrated_stars : `lsst.afw.table.SourceCatalog`
703 Star catalog with flux/magnitude columns computed from the
704 supplied PhotoCalib.
705 """
706 calibrated_sources_footprints = photo_calib.calibrateCatalog(sources_footprints)
707 exposure.maskedImage = photo_calib.calibrateImage(exposure.maskedImage)
708 identity = afwImage.PhotoCalib(1.0, photo_calib.getCalibrationErr(), bbox=exposure.getBBox())
709 exposure.setPhotoCalib(identity)
710 exposure.metadata["BUNIT"] = "nJy"
711 return calibrated_sources_footprints
714def combine_backgrounds(initial_pvi_background, sky_corr):
715 """Return the total background that was applied to the original
716 processing.
717 """
718 background = lsst.afw.math.BackgroundList()
719 for item in initial_pvi_background:
720 background.append(item)
721 for item in sky_corr:
722 background.append(item)
723 return background