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

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/>. 

21 

22__all__ = ["ReprocessVisitImageTask", "ReprocessVisitImageConfig", "combine_backgrounds"] 

23 

24import numpy as np 

25import smatch 

26 

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 

38 

39 

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. 

97 

98 # outputs 

99 sources_schema = connectionTypes.InitOutput( 

100 doc="Schema of the output sources catalog.", 

101 name="sources_schema", 

102 storageClass="SourceCatalog", 

103 ) 

104 

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 ) 

134 

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 

142 

143 

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() 

149 

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 ) 

220 

221 def setDefaults(self): 

222 super().setDefaults() 

223 

224 # No need to redo background: we have the global background model. 

225 self.detection.reEstimateBackground = False 

226 self.detection.doTempLocalBackground = False 

227 

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 

245 

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 

249 

250 # Only apply calibration fluxes, do not measure them. 

251 self.normalized_calibration_flux.do_measure_ap_corr = False 

252 

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) 

257 

258 def validate(self): 

259 super().validate() 

260 

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 ) 

270 

271 

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 """ 

276 

277 ConfigClass = ReprocessVisitImageConfig 

278 _DefaultName = "reprocessVisitImage" 

279 

280 def __init__(self, schema=None, **kwargs): 

281 super().__init__(**kwargs) 

282 

283 if schema is None: 

284 schema = afwTable.SourceTable.makeMinimalSchema() 

285 

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") 

298 

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 ) 

309 

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") 

330 

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)) 

362 

363 def runQuantum(self, butlerQC, inputRefs, outputRefs): 

364 inputs = butlerQC.get(inputRefs) 

365 id_generator = self.config.id_generator.apply(butlerQC.quantum.dataId) 

366 

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 

386 

387 # This should not happen with a properly configured execution context. 

388 assert not inputs, "runQuantum got more inputs than expected" 

389 

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") 

403 

404 if lines: 

405 msg = "\n".join(lines) 

406 raise pipeBase.UpstreamFailureNoWorkFound( 

407 f"Skipping reprocessing of detector {detector} because:\n{msg}" 

408 ) 

409 

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 

437 

438 butlerQC.put(result, outputRefs) 

439 

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. 

459 

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. 

496 

497 Returns 

498 ------- 

499 result : `lsst.pipe.base.Struct` 

500 Results as a struct with attributes: 

501 

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() 

520 

521 result.exposure = self.snap_combine.run(exposures).exposure 

522 if preliminary_mask is not None: 

523 self._copyMaskPlanes(result.exposure, preliminary_mask) 

524 

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 

529 

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) 

533 

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() 

540 

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() 

545 

546 result.exposure.setPsf(psf) 

547 result.exposure.setApCorrMap(ap_corr) 

548 result.exposure.setWcs(wcs) 

549 result.exposure.setPhotoCalib(photo_calib) 

550 

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() 

575 

576 return result 

577 

578 def _copyMaskPlanes(self, exposure, mask): 

579 """Copy mask planes from an input Mask to the final Exposure. 

580 

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 

594 

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. 

604 

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. 

619 

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()) 

626 

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 

635 

636 self.sky_sources.run(exposure.mask, id_generator.catalog_id, sources) 

637 

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) 

643 

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) 

649 

650 sources["visit"] = exposure.visitInfo.id 

651 sources["detector"] = exposure.info.getDetector().getId() 

652 

653 self._match_calib_sources(sources, calib_sources, exposure.info.getDetector().getId()) 

654 

655 return sources 

656 

657 def _match_calib_sources(self, sources, calib_sources, detector): 

658 """Match with calib_sources to set `calib_*` flags in the output 

659 catalog. 

660 

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 ) 

683 

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 

689 

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. 

693 

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 

712 

713 

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