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