Coverage for python/lsst/pipe/tasks/calibrateImage.py: 32%

190 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-11-29 10:48 +0000

1# This file is part of pipe_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 

22import numpy as np 

23 

24import lsst.afw.table as afwTable 

25import lsst.afw.image as afwImage 

26import lsst.meas.algorithms 

27import lsst.meas.algorithms.installGaussianPsf 

28import lsst.meas.algorithms.measureApCorr 

29from lsst.meas.algorithms import sourceSelector 

30import lsst.meas.astrom 

31import lsst.meas.deblender 

32import lsst.meas.extensions.shapeHSM 

33import lsst.pex.config as pexConfig 

34import lsst.pipe.base as pipeBase 

35from lsst.pipe.base import connectionTypes 

36from lsst.utils.timer import timeMethod 

37 

38from . import measurePsf, repair, setPrimaryFlags, photoCal, computeExposureSummaryStats, maskStreaks 

39 

40 

41class CalibrateImageConnections(pipeBase.PipelineTaskConnections, 

42 dimensions=("instrument", "visit", "detector")): 

43 

44 astrometry_ref_cat = connectionTypes.PrerequisiteInput( 

45 doc="Reference catalog to use for astrometric calibration.", 

46 name="gaia_dr3_20230707", 

47 storageClass="SimpleCatalog", 

48 dimensions=("skypix",), 

49 deferLoad=True, 

50 multiple=True, 

51 ) 

52 photometry_ref_cat = connectionTypes.PrerequisiteInput( 

53 doc="Reference catalog to use for photometric calibration.", 

54 name="ps1_pv3_3pi_20170110", 

55 storageClass="SimpleCatalog", 

56 dimensions=("skypix",), 

57 deferLoad=True, 

58 multiple=True 

59 ) 

60 

61 exposure = connectionTypes.Input( 

62 doc="Exposure to be calibrated, and detected and measured on.", 

63 name="postISRCCD", 

64 storageClass="Exposure", 

65 dimensions=["instrument", "exposure", "detector"], 

66 ) 

67 

68 # outputs 

69 initial_stars_schema = connectionTypes.InitOutput( 

70 doc="Schema of the output initial stars catalog.", 

71 name="initial_stars_schema", 

72 storageClass="SourceCatalog", 

73 ) 

74 

75 # TODO: We want some kind of flag on Exposures/Catalogs to make it obvious 

76 # which components had failed to be computed/persisted 

77 output_exposure = connectionTypes.Output( 

78 doc="Photometrically calibrated exposure with fitted calibrations and summary statistics.", 

79 name="initial_pvi", 

80 storageClass="ExposureF", 

81 dimensions=("instrument", "visit", "detector"), 

82 ) 

83 # TODO DM-40061: persist a parquet version of this! 

84 stars = connectionTypes.Output( 

85 doc="Catalog of unresolved sources detected on the calibrated exposure; " 

86 "includes source footprints.", 

87 name="initial_stars_footprints_detector", 

88 storageClass="SourceCatalog", 

89 dimensions=["instrument", "visit", "detector"], 

90 ) 

91 applied_photo_calib = connectionTypes.Output( 

92 doc="Photometric calibration that was applied to exposure.", 

93 name="initial_photoCalib_detector", 

94 storageClass="PhotoCalib", 

95 dimensions=("instrument", "visit", "detector"), 

96 ) 

97 background = connectionTypes.Output( 

98 doc="Background models estimated during calibration task.", 

99 name="initial_pvi_background", 

100 storageClass="Background", 

101 dimensions=("instrument", "visit", "detector"), 

102 ) 

103 

104 # Optional outputs 

105 

106 # TODO: We need to decide on what intermediate outputs we want to save, 

107 # and which to save by default. 

108 # TODO DM-40061: persist a parquet version of this! 

109 psf_stars = connectionTypes.Output( 

110 doc="Catalog of bright unresolved sources detected on the exposure used for PSF determination; " 

111 "includes source footprints.", 

112 name="initial_psf_stars_footprints", 

113 storageClass="SourceCatalog", 

114 dimensions=["instrument", "visit", "detector"], 

115 ) 

116 astrometry_matches = connectionTypes.Output( 

117 doc="Source to reference catalog matches from the astrometry solver.", 

118 name="initial_astrometry_match_detector", 

119 storageClass="Catalog", 

120 dimensions=("instrument", "visit", "detector"), 

121 ) 

122 photometry_matches = connectionTypes.Output( 

123 doc="Source to reference catalog matches from the photometry solver.", 

124 name="initial_photometry_match_detector", 

125 storageClass="Catalog", 

126 dimensions=("instrument", "visit", "detector"), 

127 ) 

128 

129 def __init__(self, *, config=None): 

130 super().__init__(config=config) 

131 if not config.optional_outputs: 

132 self.outputs.remove("psf_stars") 

133 self.outputs.remove("astrometry_matches") 

134 self.outputs.remove("photometry_matches") 

135 

136 

137class CalibrateImageConfig(pipeBase.PipelineTaskConfig, pipelineConnections=CalibrateImageConnections): 

138 optional_outputs = pexConfig.ListField( 

139 doc="Which optional outputs to save (as their connection name)?", 

140 dtype=str, 

141 # TODO: note somewhere to disable this for benchmarking, but should 

142 # we always have it on for production runs? 

143 default=["psf_stars", "astrometry_matches", "photometry_matches"], 

144 optional=True 

145 ) 

146 

147 # subtasks used during psf characterization 

148 install_simple_psf = pexConfig.ConfigurableField( 

149 target=lsst.meas.algorithms.installGaussianPsf.InstallGaussianPsfTask, 

150 doc="Task to install a simple PSF model into the input exposure to use " 

151 "when detecting bright sources for PSF estimation.", 

152 ) 

153 psf_repair = pexConfig.ConfigurableField( 

154 target=repair.RepairTask, 

155 doc="Task to repair cosmic rays on the exposure before PSF determination.", 

156 ) 

157 psf_subtract_background = pexConfig.ConfigurableField( 

158 target=lsst.meas.algorithms.SubtractBackgroundTask, 

159 doc="Task to perform intial background subtraction, before first detection pass.", 

160 ) 

161 psf_detection = pexConfig.ConfigurableField( 

162 target=lsst.meas.algorithms.SourceDetectionTask, 

163 doc="Task to detect sources for PSF determination." 

164 ) 

165 psf_source_measurement = pexConfig.ConfigurableField( 

166 target=lsst.meas.base.SingleFrameMeasurementTask, 

167 doc="Task to measure sources to be used for psf estimation." 

168 ) 

169 psf_measure_psf = pexConfig.ConfigurableField( 

170 target=measurePsf.MeasurePsfTask, 

171 doc="Task to measure the psf on bright sources." 

172 ) 

173 

174 # TODO DM-39203: we can remove aperture correction from this task once we are 

175 # using the shape-based star/galaxy code. 

176 measure_aperture_correction = pexConfig.ConfigurableField( 

177 target=lsst.meas.algorithms.measureApCorr.MeasureApCorrTask, 

178 doc="Task to compute the aperture correction from the bright stars." 

179 ) 

180 

181 # subtasks used during star measurement 

182 star_detection = pexConfig.ConfigurableField( 

183 target=lsst.meas.algorithms.SourceDetectionTask, 

184 doc="Task to detect stars to return in the output catalog." 

185 ) 

186 star_mask_streaks = pexConfig.ConfigurableField( 

187 target=maskStreaks.MaskStreaksTask, 

188 doc="Task for masking streaks. Adds a STREAK mask plane to an exposure.", 

189 ) 

190 star_deblend = pexConfig.ConfigurableField( 

191 target=lsst.meas.deblender.SourceDeblendTask, 

192 doc="Split blended sources into their components" 

193 ) 

194 star_measurement = pexConfig.ConfigurableField( 

195 target=lsst.meas.base.SingleFrameMeasurementTask, 

196 doc="Task to measure stars to return in the output catalog." 

197 ) 

198 star_apply_aperture_correction = pexConfig.ConfigurableField( 

199 target=lsst.meas.base.ApplyApCorrTask, 

200 doc="Task to apply aperture corrections to the selected stars." 

201 ) 

202 star_catalog_calculation = pexConfig.ConfigurableField( 

203 target=lsst.meas.base.CatalogCalculationTask, 

204 doc="Task to compute extendedness values on the star catalog, " 

205 "for the star selector to remove extended sources." 

206 ) 

207 star_set_primary_flags = pexConfig.ConfigurableField( 

208 target=setPrimaryFlags.SetPrimaryFlagsTask, 

209 doc="Task to add isPrimary to the catalog." 

210 ) 

211 star_selector = lsst.meas.algorithms.sourceSelectorRegistry.makeField( 

212 default="science", 

213 doc="Task to select isolated stars to use for calibration." 

214 ) 

215 

216 # final calibrations and statistics 

217 astrometry = pexConfig.ConfigurableField( 

218 target=lsst.meas.astrom.AstrometryTask, 

219 doc="Task to perform astrometric calibration to fit a WCS.", 

220 ) 

221 astrometry_ref_loader = pexConfig.ConfigField( 

222 dtype=lsst.meas.algorithms.LoadReferenceObjectsConfig, 

223 doc="Configuration of reference object loader for astrometric fit.", 

224 ) 

225 photometry = pexConfig.ConfigurableField( 

226 target=photoCal.PhotoCalTask, 

227 doc="Task to perform photometric calibration to fit a PhotoCalib.", 

228 ) 

229 photometry_ref_loader = pexConfig.ConfigField( 

230 dtype=lsst.meas.algorithms.LoadReferenceObjectsConfig, 

231 doc="Configuration of reference object loader for photometric fit.", 

232 ) 

233 

234 compute_summary_stats = pexConfig.ConfigurableField( 

235 target=computeExposureSummaryStats.ComputeExposureSummaryStatsTask, 

236 doc="Task to to compute summary statistics on the calibrated exposure." 

237 ) 

238 

239 def setDefaults(self): 

240 super().setDefaults() 

241 

242 # Use a very broad PSF here, to throughly reject CRs. 

243 # TODO investigation: a large initial psf guess may make stars look 

244 # like CRs for very good seeing images. 

245 self.install_simple_psf.fwhm = 4 

246 

247 # Only use high S/N sources for PSF determination. 

248 self.psf_detection.thresholdValue = 50.0 

249 # TODO investigation: Probably want False here, but that may require 

250 # tweaking the background spatial scale, to make it small enough to 

251 # prevent extra peaks in the wings of bright objects. 

252 self.psf_detection.doTempLocalBackground = False 

253 # NOTE: we do want reEstimateBackground=True in psf_detection, so that 

254 # each measurement step is done with the best background available. 

255 

256 # Minimal measurement plugins for PSF determination. 

257 # TODO DM-39203: We can drop GaussianFlux and PsfFlux, if we use 

258 # shapeHSM/moments for star/galaxy separation. 

259 # TODO DM-39203: we can remove aperture correction from this task once 

260 # we are using the shape-based star/galaxy code. 

261 self.psf_source_measurement.plugins = ["base_PixelFlags", 

262 "base_SdssCentroid", 

263 "ext_shapeHSM_HsmSourceMoments", 

264 "base_CircularApertureFlux", 

265 "base_GaussianFlux", 

266 "base_PsfFlux", 

267 ] 

268 self.psf_source_measurement.slots.shape = "ext_shapeHSM_HsmSourceMoments" 

269 # Only measure apertures we need for PSF measurement. 

270 # TODO DM-40064: psfex has a hard-coded value of 9 in a psfex-config 

271 # file: make that configurable and/or change it to 12 to be consistent 

272 # with our other uses? 

273 # https://github.com/lsst/meas_extensions_psfex/blob/main/config/default-lsst.psfex#L14 

274 self.psf_source_measurement.plugins["base_CircularApertureFlux"].radii = [9.0, 12.0] 

275 

276 # No extendeness information available: we need the aperture 

277 # corrections to determine that. 

278 self.measure_aperture_correction.sourceSelector["science"].doUnresolved = False 

279 self.measure_aperture_correction.sourceSelector["science"].flags.good = ["calib_psf_used"] 

280 self.measure_aperture_correction.sourceSelector["science"].flags.bad = [] 

281 

282 # TODO investigation: how faint do we have to detect, to be able to 

283 # deblend, etc? We may need star_selector to have a separate value, 

284 # and do initial detection at S/N>5.0? 

285 # Detection for good S/N for astrometry/photometry and other 

286 # downstream tasks. 

287 self.star_detection.thresholdValue = 5.0 

288 self.star_measurement.plugins = ["base_PixelFlags", 

289 "base_SdssCentroid", 

290 "ext_shapeHSM_HsmSourceMoments", 

291 'ext_shapeHSM_HsmPsfMoments', 

292 "base_GaussianFlux", 

293 "base_PsfFlux", 

294 "base_CircularApertureFlux", 

295 ] 

296 self.star_measurement.slots.psfShape = "ext_shapeHSM_HsmPsfMoments" 

297 self.star_measurement.slots.shape = "ext_shapeHSM_HsmSourceMoments" 

298 # Only measure the apertures we need for star selection. 

299 self.star_measurement.plugins["base_CircularApertureFlux"].radii = [12.0] 

300 

301 # Keep track of which footprints contain streaks 

302 self.star_measurement.plugins['base_PixelFlags'].masksFpAnywhere = ['STREAK'] 

303 self.star_measurement.plugins['base_PixelFlags'].masksFpCenter = ['STREAK'] 

304 

305 # Select isolated stars with reliable measurements and no bad flags. 

306 self.star_selector["science"].doFlags = True 

307 self.star_selector["science"].doUnresolved = True 

308 self.star_selector["science"].doSignalToNoise = True 

309 self.star_selector["science"].doIsolated = True 

310 self.star_selector["science"].signalToNoise.minimum = 10.0 

311 

312 # Use the affine WCS fitter (assumes we have a good camera geometry). 

313 self.astrometry.wcsFitter.retarget(lsst.meas.astrom.FitAffineWcsTask) 

314 # phot_g_mean is the primary Gaia band for all input bands. 

315 self.astrometry_ref_loader.anyFilterMapsToThis = "phot_g_mean" 

316 

317 # Do not subselect during fitting; we already selected good stars. 

318 self.astrometry.sourceSelector = "null" 

319 self.photometry.match.sourceSelection.retarget(sourceSelector.NullSourceSelectorTask) 

320 

321 # All sources should be good for PSF summary statistics. 

322 # TODO: These should both be changed to calib_psf_used with DM-41640. 

323 self.compute_summary_stats.starSelection = "calib_photometry_used" 

324 self.compute_summary_stats.starSelector.flags.good = ["calib_photometry_used"] 

325 

326 

327class CalibrateImageTask(pipeBase.PipelineTask): 

328 """Compute the PSF, aperture corrections, astrometric and photometric 

329 calibrations, and summary statistics for a single science exposure, and 

330 produce a catalog of brighter stars that were used to calibrate it. 

331 

332 Parameters 

333 ---------- 

334 initial_stars_schema : `lsst.afw.table.Schema` 

335 Schema of the initial_stars output catalog. 

336 """ 

337 _DefaultName = "calibrateImage" 

338 ConfigClass = CalibrateImageConfig 

339 

340 def __init__(self, initial_stars_schema=None, **kwargs): 

341 super().__init__(**kwargs) 

342 

343 # PSF determination subtasks 

344 self.makeSubtask("install_simple_psf") 

345 self.makeSubtask("psf_repair") 

346 self.makeSubtask("psf_subtract_background") 

347 self.psf_schema = afwTable.SourceTable.makeMinimalSchema() 

348 self.makeSubtask("psf_detection", schema=self.psf_schema) 

349 self.makeSubtask("psf_source_measurement", schema=self.psf_schema) 

350 self.makeSubtask("psf_measure_psf", schema=self.psf_schema) 

351 

352 self.makeSubtask("measure_aperture_correction", schema=self.psf_schema) 

353 

354 # star measurement subtasks 

355 if initial_stars_schema is None: 

356 initial_stars_schema = afwTable.SourceTable.makeMinimalSchema() 

357 afwTable.CoordKey.addErrorFields(initial_stars_schema) 

358 self.makeSubtask("star_detection", schema=initial_stars_schema) 

359 self.makeSubtask("star_mask_streaks") 

360 self.makeSubtask("star_deblend", schema=initial_stars_schema) 

361 self.makeSubtask("star_measurement", schema=initial_stars_schema) 

362 self.makeSubtask("star_apply_aperture_correction", schema=initial_stars_schema) 

363 self.makeSubtask("star_catalog_calculation", schema=initial_stars_schema) 

364 self.makeSubtask("star_set_primary_flags", schema=initial_stars_schema, isSingleFrame=True) 

365 self.makeSubtask("star_selector") 

366 

367 self.makeSubtask("astrometry", schema=initial_stars_schema) 

368 self.makeSubtask("photometry", schema=initial_stars_schema) 

369 

370 self.makeSubtask("compute_summary_stats") 

371 

372 # For the butler to persist it. 

373 self.initial_stars_schema = afwTable.SourceCatalog(initial_stars_schema) 

374 

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

376 inputs = butlerQC.get(inputRefs) 

377 

378 astrometry_loader = lsst.meas.algorithms.ReferenceObjectLoader( 

379 dataIds=[ref.datasetRef.dataId for ref in inputRefs.astrometry_ref_cat], 

380 refCats=inputs.pop("astrometry_ref_cat"), 

381 name=self.config.connections.astrometry_ref_cat, 

382 config=self.config.astrometry_ref_loader, log=self.log) 

383 self.astrometry.setRefObjLoader(astrometry_loader) 

384 

385 photometry_loader = lsst.meas.algorithms.ReferenceObjectLoader( 

386 dataIds=[ref.datasetRef.dataId for ref in inputRefs.photometry_ref_cat], 

387 refCats=inputs.pop("photometry_ref_cat"), 

388 name=self.config.connections.photometry_ref_cat, 

389 config=self.config.photometry_ref_loader, log=self.log) 

390 self.photometry.match.setRefObjLoader(photometry_loader) 

391 

392 outputs = self.run(**inputs) 

393 

394 butlerQC.put(outputs, outputRefs) 

395 

396 @timeMethod 

397 def run(self, *, exposure): 

398 """Find stars and perform psf measurement, then do a deeper detection 

399 and measurement and calibrate astrometry and photometry from that. 

400 

401 Parameters 

402 ---------- 

403 exposure : `lsst.afw.image.Exposure` 

404 Post-ISR exposure, with an initial WCS, VisitInfo, and Filter. 

405 Modified in-place during processing. 

406 

407 Returns 

408 ------- 

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

410 Results as a struct with attributes: 

411 

412 ``output_exposure`` 

413 Calibrated exposure, with pixels in nJy units. 

414 (`lsst.afw.image.Exposure`) 

415 ``stars`` 

416 Stars that were used to calibrate the exposure, with 

417 calibrated fluxes and magnitudes. 

418 (`lsst.afw.table.SourceCatalog`) 

419 ``psf_stars`` 

420 Stars that were used to determine the image PSF. 

421 (`lsst.afw.table.SourceCatalog`) 

422 ``background`` 

423 Background that was fit to the exposure when detecting 

424 ``stars``. (`lsst.afw.math.BackgroundList`) 

425 ``applied_photo_calib`` 

426 Photometric calibration that was fit to the star catalog and 

427 applied to the exposure. (`lsst.afw.image.PhotoCalib`) 

428 ``astrometry_matches`` 

429 Reference catalog stars matches used in the astrometric fit. 

430 (`list` [`lsst.afw.table.ReferenceMatch`] or `lsst.afw.table.BaseCatalog`) 

431 ``photometry_matches`` 

432 Reference catalog stars matches used in the photometric fit. 

433 (`list` [`lsst.afw.table.ReferenceMatch`] or `lsst.afw.table.BaseCatalog`) 

434 """ 

435 psf_stars, background, candidates = self._compute_psf(exposure) 

436 

437 self._measure_aperture_correction(exposure, psf_stars) 

438 

439 stars = self._find_stars(exposure, background) 

440 

441 astrometry_matches, astrometry_meta = self._fit_astrometry(exposure, stars) 

442 stars, photometry_matches, photometry_meta, photo_calib = self._fit_photometry(exposure, stars) 

443 

444 self._summarize(exposure, stars, background) 

445 

446 if self.config.optional_outputs: 

447 astrometry_matches = lsst.meas.astrom.denormalizeMatches(astrometry_matches, astrometry_meta) 

448 photometry_matches = lsst.meas.astrom.denormalizeMatches(photometry_matches, photometry_meta) 

449 

450 return pipeBase.Struct(output_exposure=exposure, 

451 stars=stars, 

452 psf_stars=psf_stars, 

453 background=background, 

454 applied_photo_calib=photo_calib, 

455 astrometry_matches=astrometry_matches, 

456 photometry_matches=photometry_matches) 

457 

458 def _compute_psf(self, exposure, guess_psf=True): 

459 """Find bright sources detected on an exposure and fit a PSF model to 

460 them, repairing likely cosmic rays before detection. 

461 

462 Repair, detect, measure, and compute PSF twice, to ensure the PSF 

463 model does not include contributions from cosmic rays. 

464 

465 Parameters 

466 ---------- 

467 exposure : `lsst.afw.image.Exposure` 

468 Exposure to detect and measure bright stars on. 

469 

470 Returns 

471 ------- 

472 sources : `lsst.afw.table.SourceCatalog` 

473 Catalog of detected bright sources. 

474 background : `lsst.afw.math.BackgroundList` 

475 Background that was fit to the exposure during detection. 

476 cell_set : `lsst.afw.math.SpatialCellSet` 

477 PSF candidates returned by the psf determiner. 

478 """ 

479 def log_psf(msg): 

480 """Log the parameters of the psf and background, with a prepended 

481 message. 

482 """ 

483 position = exposure.psf.getAveragePosition() 

484 sigma = exposure.psf.computeShape(position).getDeterminantRadius() 

485 dimensions = exposure.psf.computeImage(position).getDimensions() 

486 median_background = np.median(background.getImage().array) 

487 self.log.info("%s sigma=%0.4f, dimensions=%s; median background=%0.2f", 

488 msg, sigma, dimensions, median_background) 

489 

490 self.log.info("First pass detection with Guassian PSF FWHM=%s pixels", 

491 self.config.install_simple_psf.fwhm) 

492 self.install_simple_psf.run(exposure=exposure) 

493 

494 background = self.psf_subtract_background.run(exposure=exposure).background 

495 log_psf("Initial PSF:") 

496 self.psf_repair.run(exposure=exposure, keepCRs=True) 

497 

498 table = afwTable.SourceTable.make(self.psf_schema) 

499 # Re-estimate the background during this detection step, so that 

500 # measurement uses the most accurate background-subtraction. 

501 detections = self.psf_detection.run(table=table, exposure=exposure, background=background) 

502 self.psf_source_measurement.run(detections.sources, exposure) 

503 psf_result = self.psf_measure_psf.run(exposure=exposure, sources=detections.sources) 

504 # Replace the initial PSF with something simpler for the second 

505 # repair/detect/measure/measure_psf step: this can help it converge. 

506 self.install_simple_psf.run(exposure=exposure) 

507 

508 log_psf("Rerunning with simple PSF:") 

509 # TODO investigation: Should we only re-run repair here, to use the 

510 # new PSF? Maybe we *do* need to re-run measurement with PsfFlux, to 

511 # use the fitted PSF? 

512 # TODO investigation: do we need a separate measurement task here 

513 # for the post-psf_measure_psf step, since we only want to do PsfFlux 

514 # and GaussianFlux *after* we have a PSF? Maybe that's not relevant 

515 # once DM-39203 is merged? 

516 self.psf_repair.run(exposure=exposure, keepCRs=True) 

517 # Re-estimate the background during this detection step, so that 

518 # measurement uses the most accurate background-subtraction. 

519 detections = self.psf_detection.run(table=table, exposure=exposure, background=background) 

520 self.psf_source_measurement.run(detections.sources, exposure) 

521 psf_result = self.psf_measure_psf.run(exposure=exposure, sources=detections.sources) 

522 

523 log_psf("Final PSF:") 

524 

525 # Final repair with final PSF, removing cosmic rays this time. 

526 self.psf_repair.run(exposure=exposure) 

527 # Final measurement with the CRs removed. 

528 self.psf_source_measurement.run(detections.sources, exposure) 

529 

530 # PSF is set on exposure; only return candidates for optional saving. 

531 return detections.sources, background, psf_result.cellSet 

532 

533 def _measure_aperture_correction(self, exposure, bright_sources): 

534 """Measure and set the ApCorrMap on the Exposure, using 

535 previously-measured bright sources. 

536 

537 Parameters 

538 ---------- 

539 exposure : `lsst.afw.image.Exposure` 

540 Exposure to set the ApCorrMap on. 

541 bright_sources : `lsst.afw.table.SourceCatalog` 

542 Catalog of detected bright sources; modified to include columns 

543 necessary for point source determination for the aperture correction 

544 calculation. 

545 """ 

546 result = self.measure_aperture_correction.run(exposure, bright_sources) 

547 exposure.setApCorrMap(result.apCorrMap) 

548 

549 def _find_stars(self, exposure, background): 

550 """Detect stars on an exposure that has a PSF model, and measure their 

551 PSF, circular aperture, compensated gaussian fluxes. 

552 

553 Parameters 

554 ---------- 

555 exposure : `lsst.afw.image.Exposure` 

556 Exposure to set the ApCorrMap on. 

557 background : `lsst.afw.math.BackgroundList` 

558 Background that was fit to the exposure during detection; 

559 modified in-place during subsequent detection. 

560 

561 Returns 

562 ------- 

563 stars : `SourceCatalog` 

564 Sources that are very likely to be stars, with a limited set of 

565 measurements performed on them. 

566 """ 

567 table = afwTable.SourceTable.make(self.initial_stars_schema.schema) 

568 # Re-estimate the background during this detection step, so that 

569 # measurement uses the most accurate background-subtraction. 

570 detections = self.star_detection.run(table=table, exposure=exposure, background=background) 

571 sources = detections.sources 

572 

573 # Mask streaks 

574 self.star_mask_streaks.run(exposure) 

575 

576 # TODO investigation: Could this deblender throw away blends of non-PSF sources? 

577 self.star_deblend.run(exposure=exposure, sources=sources) 

578 # The deblender may not produce a contiguous catalog; ensure 

579 # contiguity for subsequent tasks. 

580 if not sources.isContiguous(): 

581 sources = sources.copy(deep=True) 

582 

583 # Measure everything, and use those results to select only stars. 

584 self.star_measurement.run(sources, exposure) 

585 self.star_apply_aperture_correction.run(sources, exposure.info.getApCorrMap()) 

586 self.star_catalog_calculation.run(sources) 

587 self.star_set_primary_flags.run(sources) 

588 

589 result = self.star_selector.run(sources) 

590 # The star selector may not produce a contiguous catalog. 

591 if not result.sourceCat.isContiguous(): 

592 return result.sourceCat.copy(deep=True) 

593 else: 

594 return result.sourceCat 

595 

596 def _fit_astrometry(self, exposure, stars): 

597 """Fit an astrometric model to the data and return the reference 

598 matches used in the fit, and the fitted WCS. 

599 

600 Parameters 

601 ---------- 

602 exposure : `lsst.afw.image.Exposure` 

603 Exposure that is being fit, to get PSF and other metadata from. 

604 Modified to add the fitted skyWcs. 

605 stars : `SourceCatalog` 

606 Good stars selected for use in calibration, with RA/Dec coordinates 

607 computed from the pixel positions and fitted WCS. 

608 

609 Returns 

610 ------- 

611 matches : `list` [`lsst.afw.table.ReferenceMatch`] 

612 Reference/stars matches used in the fit. 

613 """ 

614 result = self.astrometry.run(stars, exposure) 

615 return result.matches, result.matchMeta 

616 

617 def _fit_photometry(self, exposure, stars): 

618 """Fit a photometric model to the data and return the reference 

619 matches used in the fit, and the fitted PhotoCalib. 

620 

621 Parameters 

622 ---------- 

623 exposure : `lsst.afw.image.Exposure` 

624 Exposure that is being fit, to get PSF and other metadata from. 

625 Modified to be in nanojanksy units, with an assigned photoCalib 

626 identically 1. 

627 stars : `lsst.afw.table.SourceCatalog` 

628 Good stars selected for use in calibration. 

629 

630 Returns 

631 ------- 

632 calibrated_stars : `lsst.afw.table.SourceCatalog` 

633 Star catalog with flux/magnitude columns computed from the fitted 

634 photoCalib. 

635 matches : `list` [`lsst.afw.table.ReferenceMatch`] 

636 Reference/stars matches used in the fit. 

637 photoCalib : `lsst.afw.image.PhotoCalib` 

638 Photometric calibration that was fit to the star catalog. 

639 """ 

640 result = self.photometry.run(exposure, stars) 

641 calibrated_stars = result.photoCalib.calibrateCatalog(stars) 

642 exposure.maskedImage = result.photoCalib.calibrateImage(exposure.maskedImage) 

643 identity = afwImage.PhotoCalib(1.0, 

644 result.photoCalib.getCalibrationErr(), 

645 bbox=exposure.getBBox()) 

646 exposure.setPhotoCalib(identity) 

647 

648 return calibrated_stars, result.matches, result.matchMeta, result.photoCalib 

649 

650 def _summarize(self, exposure, stars, background): 

651 """Compute summary statistics on the exposure and update in-place the 

652 calibrations attached to it. 

653 

654 Parameters 

655 ---------- 

656 exposure : `lsst.afw.image.Exposure` 

657 Exposure that was calibrated, to get PSF and other metadata from. 

658 Modified to contain the computed summary statistics. 

659 stars : `SourceCatalog` 

660 Good stars selected used in calibration. 

661 background : `lsst.afw.math.BackgroundList` 

662 Background that was fit to the exposure during detection of the 

663 above stars. 

664 """ 

665 # TODO investigation: because this takes the photoCalib from the 

666 # exposure, photometric summary values may be "incorrect" (i.e. they 

667 # will reflect the ==1 nJy calibration on the exposure, not the 

668 # applied calibration). This needs to be checked. 

669 summary = self.compute_summary_stats.run(exposure, stars, background) 

670 exposure.info.setSummaryStats(summary)