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

179 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-17 10:06 +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 lsst.afw.table as afwTable 

23import lsst.afw.image as afwImage 

24import lsst.meas.algorithms 

25import lsst.meas.algorithms.installGaussianPsf 

26import lsst.meas.algorithms.measureApCorr 

27from lsst.meas.algorithms import sourceSelector 

28import lsst.meas.astrom 

29import lsst.meas.deblender 

30import lsst.meas.extensions.shapeHSM 

31import lsst.pex.config as pexConfig 

32import lsst.pipe.base as pipeBase 

33from lsst.pipe.base import connectionTypes 

34from lsst.utils.timer import timeMethod 

35 

36from . import measurePsf, repair, setPrimaryFlags, photoCal, computeExposureSummaryStats 

37 

38 

39class CalibrateImageConnections(pipeBase.PipelineTaskConnections, 

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

41 

42 astrometry_ref_cat = connectionTypes.PrerequisiteInput( 

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

44 name="gaia_dr3_20230707", 

45 storageClass="SimpleCatalog", 

46 dimensions=("skypix",), 

47 deferLoad=True, 

48 multiple=True, 

49 ) 

50 photometry_ref_cat = connectionTypes.PrerequisiteInput( 

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

52 name="ps1_pv3_3pi_20170110", 

53 storageClass="SimpleCatalog", 

54 dimensions=("skypix",), 

55 deferLoad=True, 

56 multiple=True 

57 ) 

58 

59 exposure = connectionTypes.Input( 

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

61 name="postISRCCD", 

62 storageClass="Exposure", 

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

64 ) 

65 

66 # outputs 

67 initial_stars_schema = connectionTypes.InitOutput( 

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

69 name="initial_stars_schema", 

70 storageClass="SourceCatalog", 

71 ) 

72 

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

74 # which components had failed to be computed/persisted 

75 output_exposure = connectionTypes.Output( 

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

77 name="initial_pvi", 

78 storageClass="ExposureF", 

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

80 ) 

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

82 stars = connectionTypes.Output( 

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

84 "includes source footprints.", 

85 name="initial_stars_footprints_detector", 

86 storageClass="SourceCatalog", 

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

88 ) 

89 applied_photo_calib = connectionTypes.Output( 

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

91 name="initial_photoCalib_detector", 

92 storageClass="PhotoCalib", 

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

94 ) 

95 background = connectionTypes.Output( 

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

97 name="initial_pvi_background", 

98 storageClass="Background", 

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

100 ) 

101 

102 # Optional outputs 

103 

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

105 # and which to save by default. 

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

107 psf_stars = connectionTypes.Output( 

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

109 "includes source footprints.", 

110 name="initial_psf_stars_footprints", 

111 storageClass="SourceCatalog", 

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

113 ) 

114 astrometry_matches = connectionTypes.Output( 

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

116 name="initial_astrometry_match_detector", 

117 storageClass="Catalog", 

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

119 ) 

120 photometry_matches = connectionTypes.Output( 

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

122 name="initial_photometry_match_detector", 

123 storageClass="Catalog", 

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

125 ) 

126 

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

128 super().__init__(config=config) 

129 if not config.optional_outputs: 

130 self.outputs.remove("psf_stars") 

131 self.outputs.remove("astrometry_matches") 

132 self.outputs.remove("photometry_matches") 

133 

134 

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

136 optional_outputs = pexConfig.ListField( 

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

138 dtype=str, 

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

140 # we always have it on for production runs? 

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

142 optional=True 

143 ) 

144 

145 # subtasks used during psf characterization 

146 install_simple_psf = pexConfig.ConfigurableField( 

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

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

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

150 ) 

151 psf_repair = pexConfig.ConfigurableField( 

152 target=repair.RepairTask, 

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

154 ) 

155 psf_subtract_background = pexConfig.ConfigurableField( 

156 target=lsst.meas.algorithms.SubtractBackgroundTask, 

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

158 ) 

159 psf_detection = pexConfig.ConfigurableField( 

160 target=lsst.meas.algorithms.SourceDetectionTask, 

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

162 ) 

163 psf_source_measurement = pexConfig.ConfigurableField( 

164 target=lsst.meas.base.SingleFrameMeasurementTask, 

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

166 ) 

167 psf_measure_psf = pexConfig.ConfigurableField( 

168 target=measurePsf.MeasurePsfTask, 

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

170 ) 

171 

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

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

174 measure_aperture_correction = pexConfig.ConfigurableField( 

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

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

177 ) 

178 

179 # subtasks used during star measurement 

180 star_detection = pexConfig.ConfigurableField( 

181 target=lsst.meas.algorithms.SourceDetectionTask, 

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

183 ) 

184 star_deblend = pexConfig.ConfigurableField( 

185 target=lsst.meas.deblender.SourceDeblendTask, 

186 doc="Split blended sources into their components" 

187 ) 

188 star_measurement = pexConfig.ConfigurableField( 

189 target=lsst.meas.base.SingleFrameMeasurementTask, 

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

191 ) 

192 star_apply_aperture_correction = pexConfig.ConfigurableField( 

193 target=lsst.meas.base.ApplyApCorrTask, 

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

195 ) 

196 star_catalog_calculation = pexConfig.ConfigurableField( 

197 target=lsst.meas.base.CatalogCalculationTask, 

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

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

200 ) 

201 star_set_primary_flags = pexConfig.ConfigurableField( 

202 target=setPrimaryFlags.SetPrimaryFlagsTask, 

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

204 ) 

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

206 default="science", 

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

208 ) 

209 

210 # final calibrations and statistics 

211 astrometry = pexConfig.ConfigurableField( 

212 target=lsst.meas.astrom.AstrometryTask, 

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

214 ) 

215 astrometry_ref_loader = pexConfig.ConfigField( 

216 dtype=lsst.meas.algorithms.LoadReferenceObjectsConfig, 

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

218 ) 

219 photometry = pexConfig.ConfigurableField( 

220 target=photoCal.PhotoCalTask, 

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

222 ) 

223 photometry_ref_loader = pexConfig.ConfigField( 

224 dtype=lsst.meas.algorithms.LoadReferenceObjectsConfig, 

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

226 ) 

227 

228 compute_summary_stats = pexConfig.ConfigurableField( 

229 target=computeExposureSummaryStats.ComputeExposureSummaryStatsTask, 

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

231 ) 

232 

233 def setDefaults(self): 

234 super().setDefaults() 

235 

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

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

238 # like CRs for very good seeing images. 

239 self.install_simple_psf.fwhm = 4 

240 

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

242 self.psf_detection.thresholdValue = 50.0 

243 self.psf_detection.thresholdType = "pixel_stdev" 

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

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

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

247 self.psf_detection.doTempLocalBackground = False 

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

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

250 

251 # Minimal measurement plugins for PSF determination. 

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

253 # shapeHSM/moments for star/galaxy separation. 

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

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

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

257 "base_SdssCentroid", 

258 "ext_shapeHSM_HsmSourceMoments", 

259 "base_CircularApertureFlux", 

260 "base_GaussianFlux", 

261 "base_PsfFlux", 

262 ] 

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

264 # Only measure apertures we need for PSF measurement. 

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

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

267 # with our other uses? 

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

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

270 

271 self.psf_measure_psf.starSelector["objectSize"].doFluxLimit = False 

272 self.psf_measure_psf.starSelector["objectSize"].doSignalToNoiseLimit = True 

273 

274 # No extendeness information available: we need the aperture 

275 # corrections to determine that. 

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

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

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

279 

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

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

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

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

284 # downstream tasks. 

285 self.star_detection.thresholdValue = 10.0 

286 self.star_detection.thresholdType = "pixel_stdev" 

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

288 "base_SdssCentroid", 

289 "ext_shapeHSM_HsmSourceMoments", 

290 'ext_shapeHSM_HsmPsfMoments', 

291 "base_GaussianFlux", 

292 "base_PsfFlux", 

293 "base_CircularApertureFlux", 

294 ] 

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

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

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

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

299 # Restrict footprint area to prevent memory blowup on huge footprints. 

300 self.star_deblend.maxFootprintArea = 10000 

301 

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

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

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

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

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

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

308 

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

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

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

312 self.astrometry_ref_loader.anyFilterMapsToThis = "phot_g_mean" 

313 

314 # Reject magnitude outliers (TODO DM-39796: should be task default) 

315 self.astrometry.doMagnitudeOutlierRejection = True 

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 self.compute_summary_stats.starSelection = "calib_photometry_used" 

323 

324 

325class CalibrateImageTask(pipeBase.PipelineTask): 

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

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

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

329 

330 Parameters 

331 ---------- 

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

333 Schema of the initial_stars output catalog. 

334 """ 

335 _DefaultName = "calibrateImage" 

336 ConfigClass = CalibrateImageConfig 

337 

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

339 super().__init__(**kwargs) 

340 

341 # PSF determination subtasks 

342 self.makeSubtask("install_simple_psf") 

343 self.makeSubtask("psf_repair") 

344 self.makeSubtask("psf_subtract_background") 

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

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

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

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

349 

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

351 

352 # star measurement subtasks 

353 if initial_stars_schema is None: 

354 initial_stars_schema = afwTable.SourceTable.makeMinimalSchema() 

355 afwTable.CoordKey.addErrorFields(initial_stars_schema) 

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

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

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

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

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

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

362 self.makeSubtask("star_selector") 

363 

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

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

366 

367 self.makeSubtask("compute_summary_stats") 

368 

369 # For the butler to persist it. 

370 self.initial_stars_schema = afwTable.SourceCatalog(initial_stars_schema) 

371 

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

373 inputs = butlerQC.get(inputRefs) 

374 

375 astrometry_loader = lsst.meas.algorithms.ReferenceObjectLoader( 

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

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

378 name=self.config.connections.astrometry_ref_cat, 

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

380 self.astrometry.setRefObjLoader(astrometry_loader) 

381 

382 photometry_loader = lsst.meas.algorithms.ReferenceObjectLoader( 

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

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

385 name=self.config.connections.photometry_ref_cat, 

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

387 self.photometry.match.setRefObjLoader(photometry_loader) 

388 

389 outputs = self.run(**inputs) 

390 

391 butlerQC.put(outputs, outputRefs) 

392 

393 @timeMethod 

394 def run(self, *, exposure): 

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

396 and measurement and calibrate astrometry and photometry from that. 

397 

398 Parameters 

399 ---------- 

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

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

402 Modified in-place during processing. 

403 

404 Returns 

405 ------- 

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

407 Results as a struct with attributes: 

408 

409 ``output_exposure`` 

410 Calibrated exposure, with pixels in nJy units. 

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

412 ``stars`` 

413 Stars that were used to calibrate the exposure, with 

414 calibrated fluxes and magnitudes. 

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

416 ``psf_stars`` 

417 Stars that were used to determine the image PSF. 

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

419 ``background`` 

420 Background that was fit to the exposure when detecting 

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

422 ``applied_photo_calib`` 

423 Photometric calibration that was fit to the star catalog and 

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

425 ``astrometry_matches`` 

426 Reference catalog stars matches used in the astrometric fit. 

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

428 ``photometry_matches`` 

429 Reference catalog stars matches used in the photometric fit. 

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

431 """ 

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

433 

434 self._measure_aperture_correction(exposure, psf_stars) 

435 

436 stars = self._find_stars(exposure, background) 

437 

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

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

440 

441 self._summarize(exposure, stars, background) 

442 

443 if self.config.optional_outputs: 

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

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

446 

447 return pipeBase.Struct(output_exposure=exposure, 

448 stars=stars, 

449 psf_stars=psf_stars, 

450 background=background, 

451 applied_photo_calib=photo_calib, 

452 astrometry_matches=astrometry_matches, 

453 photometry_matches=photometry_matches) 

454 

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

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

457 them, repairing likely cosmic rays before detection. 

458 

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

460 model does not include contributions from cosmic rays. 

461 

462 Parameters 

463 ---------- 

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

465 Exposure to detect and measure bright stars on. 

466 

467 Returns 

468 ------- 

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

470 Catalog of detected bright sources. 

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

472 Background that was fit to the exposure during detection. 

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

474 PSF candidates returned by the psf determiner. 

475 """ 

476 self.log.info("First pass detection with Guassian PSF FWHM=%s", self.config.install_simple_psf.fwhm) 

477 self.install_simple_psf.run(exposure=exposure) 

478 

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

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

481 

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

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

484 # measurement uses the most accurate background-subtraction. 

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

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

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

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

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

490 self.install_simple_psf.run(exposure=exposure) 

491 

492 self.log.info("Re-running repair, detection, and PSF measurement using new simple PSF.") 

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

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

495 # use the fitted PSF? 

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

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

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

499 # once DM-39203 is merged? 

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

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

502 # measurement uses the most accurate background-subtraction. 

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

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

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

506 

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

508 return detections.sources, background, psf_result.cellSet 

509 

510 def _measure_aperture_correction(self, exposure, bright_sources): 

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

512 previously-measured bright sources. 

513 

514 Parameters 

515 ---------- 

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

517 Exposure to set the ApCorrMap on. 

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

519 Catalog of detected bright sources; modified to include columns 

520 necessary for point source determination for the aperture correction 

521 calculation. 

522 """ 

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

524 exposure.setApCorrMap(result.apCorrMap) 

525 

526 def _find_stars(self, exposure, background): 

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

528 PSF, circular aperture, compensated gaussian fluxes. 

529 

530 Parameters 

531 ---------- 

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

533 Exposure to set the ApCorrMap on. 

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

535 Background that was fit to the exposure during detection; 

536 modified in-place during subsequent detection. 

537 

538 Returns 

539 ------- 

540 stars : `SourceCatalog` 

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

542 measurements performed on them. 

543 """ 

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

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

546 # measurement uses the most accurate background-subtraction. 

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

548 sources = detections.sources 

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

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

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

552 # contiguity for subsequent tasks. 

553 if not sources.isContiguous(): 

554 sources = sources.copy(deep=True) 

555 

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

557 self.star_measurement.run(sources, exposure) 

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

559 self.star_catalog_calculation.run(sources) 

560 self.star_set_primary_flags.run(sources) 

561 

562 result = self.star_selector.run(sources) 

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

564 if not result.sourceCat.isContiguous(): 

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

566 else: 

567 return result.sourceCat 

568 

569 def _fit_astrometry(self, exposure, stars): 

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

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

572 

573 Parameters 

574 ---------- 

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

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

577 Modified to add the fitted skyWcs. 

578 stars : `SourceCatalog` 

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

580 computed from the pixel positions and fitted WCS. 

581 

582 Returns 

583 ------- 

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

585 Reference/stars matches used in the fit. 

586 """ 

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

588 return result.matches, result.matchMeta 

589 

590 def _fit_photometry(self, exposure, stars): 

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

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

593 

594 Parameters 

595 ---------- 

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

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

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

599 identically 1. 

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

601 Good stars selected for use in calibration. 

602 

603 Returns 

604 ------- 

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

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

607 photoCalib. 

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

609 Reference/stars matches used in the fit. 

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

611 Photometric calibration that was fit to the star catalog. 

612 """ 

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

614 calibrated_stars = result.photoCalib.calibrateCatalog(stars) 

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

616 identity = afwImage.PhotoCalib(1.0, 

617 result.photoCalib.getCalibrationErr(), 

618 bbox=exposure.getBBox()) 

619 exposure.setPhotoCalib(identity) 

620 

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

622 

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

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

625 calibrations attached to it. 

626 

627 Parameters 

628 ---------- 

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

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

631 Modified to contain the computed summary statistics. 

632 stars : `SourceCatalog` 

633 Good stars selected used in calibration. 

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

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

636 above stars. 

637 """ 

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

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

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

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

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

643 exposure.info.setSummaryStats(summary)