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

233 statements  

« prev     ^ index     » next       coverage.py v7.4.1, created at 2024-02-14 10:04 +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 collections.abc 

23 

24import numpy as np 

25 

26import lsst.afw.table as afwTable 

27import lsst.afw.image as afwImage 

28import lsst.meas.algorithms 

29import lsst.meas.algorithms.installGaussianPsf 

30import lsst.meas.algorithms.measureApCorr 

31from lsst.meas.algorithms import sourceSelector 

32import lsst.meas.astrom 

33import lsst.meas.deblender 

34import lsst.meas.extensions.shapeHSM 

35import lsst.pex.config as pexConfig 

36import lsst.pipe.base as pipeBase 

37from lsst.pipe.base import connectionTypes 

38from lsst.utils.timer import timeMethod 

39 

40from . import measurePsf, repair, setPrimaryFlags, photoCal, \ 

41 computeExposureSummaryStats, maskStreaks, snapCombine 

42 

43 

44class CalibrateImageConnections(pipeBase.PipelineTaskConnections, 

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

46 

47 astrometry_ref_cat = connectionTypes.PrerequisiteInput( 

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

49 name="gaia_dr3_20230707", 

50 storageClass="SimpleCatalog", 

51 dimensions=("skypix",), 

52 deferLoad=True, 

53 multiple=True, 

54 ) 

55 photometry_ref_cat = connectionTypes.PrerequisiteInput( 

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

57 name="ps1_pv3_3pi_20170110", 

58 storageClass="SimpleCatalog", 

59 dimensions=("skypix",), 

60 deferLoad=True, 

61 multiple=True 

62 ) 

63 

64 exposures = connectionTypes.Input( 

65 doc="Exposure (or two snaps) to be calibrated, and detected and measured on.", 

66 name="postISRCCD", 

67 storageClass="Exposure", 

68 multiple=True, # to handle 1 exposure or 2 snaps 

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

70 ) 

71 

72 # outputs 

73 initial_stars_schema = connectionTypes.InitOutput( 

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

75 name="initial_stars_schema", 

76 storageClass="SourceCatalog", 

77 ) 

78 

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

80 # which components had failed to be computed/persisted 

81 output_exposure = connectionTypes.Output( 

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

83 name="initial_pvi", 

84 storageClass="ExposureF", 

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

86 ) 

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

88 stars = connectionTypes.Output( 

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

90 "includes source footprints.", 

91 name="initial_stars_footprints_detector", 

92 storageClass="SourceCatalog", 

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

94 ) 

95 applied_photo_calib = connectionTypes.Output( 

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

97 name="initial_photoCalib_detector", 

98 storageClass="PhotoCalib", 

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

100 ) 

101 background = connectionTypes.Output( 

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

103 name="initial_pvi_background", 

104 storageClass="Background", 

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

106 ) 

107 

108 # Optional outputs 

109 

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

111 # and which to save by default. 

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

113 psf_stars = connectionTypes.Output( 

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

115 "includes source footprints.", 

116 name="initial_psf_stars_footprints", 

117 storageClass="SourceCatalog", 

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

119 ) 

120 astrometry_matches = connectionTypes.Output( 

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

122 name="initial_astrometry_match_detector", 

123 storageClass="Catalog", 

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

125 ) 

126 photometry_matches = connectionTypes.Output( 

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

128 name="initial_photometry_match_detector", 

129 storageClass="Catalog", 

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

131 ) 

132 

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

134 super().__init__(config=config) 

135 if not config.optional_outputs: 

136 self.outputs.remove("psf_stars") 

137 self.outputs.remove("astrometry_matches") 

138 self.outputs.remove("photometry_matches") 

139 

140 

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

142 optional_outputs = pexConfig.ListField( 

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

144 dtype=str, 

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

146 # we always have it on for production runs? 

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

148 optional=True 

149 ) 

150 

151 snap_combine = pexConfig.ConfigurableField( 

152 target=snapCombine.SnapCombineTask, 

153 doc="Task to combine two snaps to make one exposure.", 

154 ) 

155 

156 # subtasks used during psf characterization 

157 install_simple_psf = pexConfig.ConfigurableField( 

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

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

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

161 ) 

162 psf_repair = pexConfig.ConfigurableField( 

163 target=repair.RepairTask, 

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

165 ) 

166 psf_subtract_background = pexConfig.ConfigurableField( 

167 target=lsst.meas.algorithms.SubtractBackgroundTask, 

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

169 ) 

170 psf_detection = pexConfig.ConfigurableField( 

171 target=lsst.meas.algorithms.SourceDetectionTask, 

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

173 ) 

174 psf_source_measurement = pexConfig.ConfigurableField( 

175 target=lsst.meas.base.SingleFrameMeasurementTask, 

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

177 ) 

178 psf_measure_psf = pexConfig.ConfigurableField( 

179 target=measurePsf.MeasurePsfTask, 

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

181 ) 

182 

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

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

185 measure_aperture_correction = pexConfig.ConfigurableField( 

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

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

188 ) 

189 

190 # subtasks used during star measurement 

191 star_detection = pexConfig.ConfigurableField( 

192 target=lsst.meas.algorithms.SourceDetectionTask, 

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

194 ) 

195 star_mask_streaks = pexConfig.ConfigurableField( 

196 target=maskStreaks.MaskStreaksTask, 

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

198 ) 

199 star_deblend = pexConfig.ConfigurableField( 

200 target=lsst.meas.deblender.SourceDeblendTask, 

201 doc="Split blended sources into their components" 

202 ) 

203 star_measurement = pexConfig.ConfigurableField( 

204 target=lsst.meas.base.SingleFrameMeasurementTask, 

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

206 ) 

207 star_apply_aperture_correction = pexConfig.ConfigurableField( 

208 target=lsst.meas.base.ApplyApCorrTask, 

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

210 ) 

211 star_catalog_calculation = pexConfig.ConfigurableField( 

212 target=lsst.meas.base.CatalogCalculationTask, 

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

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

215 ) 

216 star_set_primary_flags = pexConfig.ConfigurableField( 

217 target=setPrimaryFlags.SetPrimaryFlagsTask, 

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

219 ) 

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

221 default="science", 

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

223 ) 

224 

225 # final calibrations and statistics 

226 astrometry = pexConfig.ConfigurableField( 

227 target=lsst.meas.astrom.AstrometryTask, 

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

229 ) 

230 astrometry_ref_loader = pexConfig.ConfigField( 

231 dtype=lsst.meas.algorithms.LoadReferenceObjectsConfig, 

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

233 ) 

234 photometry = pexConfig.ConfigurableField( 

235 target=photoCal.PhotoCalTask, 

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

237 ) 

238 photometry_ref_loader = pexConfig.ConfigField( 

239 dtype=lsst.meas.algorithms.LoadReferenceObjectsConfig, 

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

241 ) 

242 

243 compute_summary_stats = pexConfig.ConfigurableField( 

244 target=computeExposureSummaryStats.ComputeExposureSummaryStatsTask, 

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

246 ) 

247 

248 def setDefaults(self): 

249 super().setDefaults() 

250 

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

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

253 # like CRs for very good seeing images. 

254 self.install_simple_psf.fwhm = 4 

255 

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

257 self.psf_detection.thresholdValue = 50.0 

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

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

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

261 self.psf_detection.doTempLocalBackground = False 

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

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

264 

265 # Minimal measurement plugins for PSF determination. 

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

267 # shapeHSM/moments for star/galaxy separation. 

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

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

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

271 "base_SdssCentroid", 

272 "ext_shapeHSM_HsmSourceMoments", 

273 "base_CircularApertureFlux", 

274 "base_GaussianFlux", 

275 "base_PsfFlux", 

276 "base_ClassificationSizeExtendedness", 

277 ] 

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

279 # Only measure apertures we need for PSF measurement. 

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

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

282 # with our other uses? 

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

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

285 

286 # No extendeness information available: we need the aperture 

287 # corrections to determine that. 

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

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

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

291 

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

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

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

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

296 # downstream tasks. 

297 self.star_detection.thresholdValue = 5.0 

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

299 "base_SdssCentroid", 

300 "ext_shapeHSM_HsmSourceMoments", 

301 'ext_shapeHSM_HsmPsfMoments', 

302 "base_GaussianFlux", 

303 "base_PsfFlux", 

304 "base_CircularApertureFlux", 

305 "base_ClassificationSizeExtendedness", 

306 ] 

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

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

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

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

311 

312 # Keep track of which footprints contain streaks 

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

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

315 

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

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

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

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

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

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

322 

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

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

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

326 self.astrometry_ref_loader.anyFilterMapsToThis = "phot_g_mean" 

327 

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

329 self.astrometry.sourceSelector = "null" 

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

331 

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

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

334 self.compute_summary_stats.starSelection = "calib_photometry_used" 

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

336 

337 

338class CalibrateImageTask(pipeBase.PipelineTask): 

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

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

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

342 

343 Parameters 

344 ---------- 

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

346 Schema of the initial_stars output catalog. 

347 """ 

348 _DefaultName = "calibrateImage" 

349 ConfigClass = CalibrateImageConfig 

350 

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

352 super().__init__(**kwargs) 

353 

354 self.makeSubtask("snap_combine") 

355 

356 # PSF determination subtasks 

357 self.makeSubtask("install_simple_psf") 

358 self.makeSubtask("psf_repair") 

359 self.makeSubtask("psf_subtract_background") 

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

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

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

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

364 

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

366 

367 # star measurement subtasks 

368 if initial_stars_schema is None: 

369 initial_stars_schema = afwTable.SourceTable.makeMinimalSchema() 

370 

371 # These fields let us track which sources were used for psf and 

372 # aperture correction calculations. 

373 self.psf_fields = ("calib_psf_candidate", "calib_psf_used", "calib_psf_reserved", 

374 # TODO DM-39203: these can be removed once apcorr is gone. 

375 "apcorr_slot_CalibFlux_used", "apcorr_base_GaussianFlux_used", 

376 "apcorr_base_PsfFlux_used") 

377 for field in self.psf_fields: 

378 item = self.psf_schema.find(field) 

379 initial_stars_schema.addField(item.getField()) 

380 

381 afwTable.CoordKey.addErrorFields(initial_stars_schema) 

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

383 self.makeSubtask("star_mask_streaks") 

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

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

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

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

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

389 self.makeSubtask("star_selector") 

390 

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

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

393 

394 self.makeSubtask("compute_summary_stats") 

395 

396 # For the butler to persist it. 

397 self.initial_stars_schema = afwTable.SourceCatalog(initial_stars_schema) 

398 

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

400 inputs = butlerQC.get(inputRefs) 

401 

402 astrometry_loader = lsst.meas.algorithms.ReferenceObjectLoader( 

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

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

405 name=self.config.connections.astrometry_ref_cat, 

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

407 self.astrometry.setRefObjLoader(astrometry_loader) 

408 

409 photometry_loader = lsst.meas.algorithms.ReferenceObjectLoader( 

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

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

412 name=self.config.connections.photometry_ref_cat, 

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

414 self.photometry.match.setRefObjLoader(photometry_loader) 

415 

416 outputs = self.run(**inputs) 

417 

418 butlerQC.put(outputs, outputRefs) 

419 

420 @timeMethod 

421 def run(self, *, exposures): 

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

423 and measurement and calibrate astrometry and photometry from that. 

424 

425 Parameters 

426 ---------- 

427 exposures : `lsst.afw.image.Exposure` or `list` [`lsst.afw.image.Exposure`] 

428 Post-ISR exposure(s), with an initial WCS, VisitInfo, and Filter. 

429 Modified in-place during processing if only one is passed. 

430 If two exposures are passed, treat them as snaps and combine 

431 before doing further processing. 

432 

433 Returns 

434 ------- 

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

436 Results as a struct with attributes: 

437 

438 ``output_exposure`` 

439 Calibrated exposure, with pixels in nJy units. 

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

441 ``stars`` 

442 Stars that were used to calibrate the exposure, with 

443 calibrated fluxes and magnitudes. 

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

445 ``psf_stars`` 

446 Stars that were used to determine the image PSF. 

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

448 ``background`` 

449 Background that was fit to the exposure when detecting 

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

451 ``applied_photo_calib`` 

452 Photometric calibration that was fit to the star catalog and 

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

454 ``astrometry_matches`` 

455 Reference catalog stars matches used in the astrometric fit. 

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

457 ``photometry_matches`` 

458 Reference catalog stars matches used in the photometric fit. 

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

460 """ 

461 exposure = self._handle_snaps(exposures) 

462 

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

464 

465 self._measure_aperture_correction(exposure, psf_stars) 

466 

467 stars = self._find_stars(exposure, background) 

468 

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

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

471 

472 self._summarize(exposure, stars, background) 

473 

474 if self.config.optional_outputs: 

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

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

477 

478 return pipeBase.Struct(output_exposure=exposure, 

479 stars=stars, 

480 psf_stars=psf_stars, 

481 background=background, 

482 applied_photo_calib=photo_calib, 

483 astrometry_matches=astrometry_matches, 

484 photometry_matches=photometry_matches) 

485 

486 def _handle_snaps(self, exposure): 

487 """Combine two snaps into one exposure, or return a single exposure. 

488 

489 Parameters 

490 ---------- 

491 exposure : `lsst.afw.image.Exposure` or `list` [`lsst.afw.image.Exposure]` 

492 One or two exposures to combine as snaps. 

493 

494 Returns 

495 ------- 

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

497 A single exposure to continue processing. 

498 

499 Raises 

500 ------ 

501 RuntimeError 

502 Raised if input does not contain either 1 or 2 exposures. 

503 """ 

504 if isinstance(exposure, lsst.afw.image.Exposure): 

505 return exposure 

506 

507 if isinstance(exposure, collections.abc.Sequence): 

508 match len(exposure): 

509 case 1: 

510 return exposure[0] 

511 case 2: 

512 return self.snap_combine.run(exposure[0], exposure[1]).exposure 

513 case n: 

514 raise RuntimeError(f"Can only process 1 or 2 snaps, not {n}.") 

515 

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

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

518 them, repairing likely cosmic rays before detection. 

519 

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

521 model does not include contributions from cosmic rays. 

522 

523 Parameters 

524 ---------- 

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

526 Exposure to detect and measure bright stars on. 

527 

528 Returns 

529 ------- 

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

531 Catalog of detected bright sources. 

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

533 Background that was fit to the exposure during detection. 

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

535 PSF candidates returned by the psf determiner. 

536 """ 

537 def log_psf(msg): 

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

539 message. 

540 """ 

541 position = exposure.psf.getAveragePosition() 

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

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

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

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

546 msg, sigma, dimensions, median_background) 

547 

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

549 self.config.install_simple_psf.fwhm) 

550 self.install_simple_psf.run(exposure=exposure) 

551 

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

553 log_psf("Initial PSF:") 

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

555 

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

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

558 # measurement uses the most accurate background-subtraction. 

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

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

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

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

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

564 self.install_simple_psf.run(exposure=exposure) 

565 

566 log_psf("Rerunning with simple PSF:") 

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

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

569 # use the fitted PSF? 

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

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

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

573 # once DM-39203 is merged? 

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

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

576 # measurement uses the most accurate background-subtraction. 

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

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

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

580 

581 log_psf("Final PSF:") 

582 

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

584 self.psf_repair.run(exposure=exposure) 

585 # Final measurement with the CRs removed. 

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

587 

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

589 return detections.sources, background, psf_result.cellSet 

590 

591 def _measure_aperture_correction(self, exposure, bright_sources): 

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

593 previously-measured bright sources. 

594 

595 Parameters 

596 ---------- 

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

598 Exposure to set the ApCorrMap on. 

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

600 Catalog of detected bright sources; modified to include columns 

601 necessary for point source determination for the aperture correction 

602 calculation. 

603 """ 

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

605 exposure.setApCorrMap(result.apCorrMap) 

606 

607 def _find_stars(self, exposure, background): 

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

609 PSF, circular aperture, compensated gaussian fluxes. 

610 

611 Parameters 

612 ---------- 

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

614 Exposure to set the ApCorrMap on. 

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

616 Background that was fit to the exposure during detection; 

617 modified in-place during subsequent detection. 

618 

619 Returns 

620 ------- 

621 stars : `SourceCatalog` 

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

623 measurements performed on them. 

624 """ 

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

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

627 # measurement uses the most accurate background-subtraction. 

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

629 sources = detections.sources 

630 

631 # Mask streaks 

632 self.star_mask_streaks.run(exposure) 

633 

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

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

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

637 # contiguity for subsequent tasks. 

638 if not sources.isContiguous(): 

639 sources = sources.copy(deep=True) 

640 

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

642 self.star_measurement.run(sources, exposure) 

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

644 self.star_catalog_calculation.run(sources) 

645 self.star_set_primary_flags.run(sources) 

646 

647 result = self.star_selector.run(sources) 

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

649 if not result.sourceCat.isContiguous(): 

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

651 else: 

652 return result.sourceCat 

653 

654 def _match_psf_stars(self, psf_stars, stars): 

655 """Match calibration stars to psf stars, to identify which were psf 

656 candidates, and which were used or reserved during psf measurement. 

657 

658 Parameters 

659 ---------- 

660 psf_stars : `lsst.afw.table.SourceCatalog` 

661 PSF candidate stars that were sent to the psf determiner. Used to 

662 populate psf-related flag fields. 

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

664 Stars that will be used for calibration; psf-related fields will 

665 be updated in-place. 

666 

667 Notes 

668 ----- 

669 This code was adapted from CalibrateTask.copyIcSourceFields(). 

670 """ 

671 control = afwTable.MatchControl() 

672 # Return all matched objects, to separate blends. 

673 control.findOnlyClosest = False 

674 matches = afwTable.matchXy(psf_stars, stars, 3.0, control) 

675 deblend_key = stars.schema["deblend_nChild"].asKey() 

676 matches = [m for m in matches if m[1].get(deblend_key) == 0] 

677 

678 # Because we had to allow multiple matches to handle parents, we now 

679 # need to prune to the best (closest) matches. 

680 # Closest matches is a dict of psf_stars source ID to Match record 

681 # (psf_stars source, sourceCat source, distance in pixels). 

682 best = {} 

683 for match0, match1, d in matches: 

684 id0 = match0.getId() 

685 match = best.get(id0) 

686 if match is None or d <= match[2]: 

687 best[id0] = (match0, match1, d) 

688 matches = list(best.values()) 

689 ids = np.array([(match0.getId(), match1.getId()) for match0, match1, d in matches]).T 

690 

691 # Check that no stars sources are listed twice; we already know 

692 # that each match has a unique psf_stars id, due to using as the key 

693 # in best above. 

694 n_matches = len(matches) 

695 n_unique = len(set(m[1].getId() for m in matches)) 

696 if n_unique != n_matches: 

697 self.log.warning("%d psf_stars matched only %d stars; ", 

698 n_matches, n_unique) 

699 

700 # The indices of the IDs, so we can update the flag fields as arrays. 

701 idx0 = np.searchsorted(psf_stars["id"], ids[0]) 

702 idx1 = np.searchsorted(stars["id"], ids[1]) 

703 for field in self.psf_fields: 

704 result = np.zeros(len(stars), dtype=bool) 

705 result[idx0] = psf_stars[field][idx1] 

706 stars[field] = result 

707 

708 def _fit_astrometry(self, exposure, stars): 

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

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

711 

712 Parameters 

713 ---------- 

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

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

716 Modified to add the fitted skyWcs. 

717 stars : `SourceCatalog` 

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

719 computed from the pixel positions and fitted WCS. 

720 

721 Returns 

722 ------- 

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

724 Reference/stars matches used in the fit. 

725 """ 

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

727 return result.matches, result.matchMeta 

728 

729 def _fit_photometry(self, exposure, stars): 

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

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

732 

733 Parameters 

734 ---------- 

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

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

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

738 identically 1. 

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

740 Good stars selected for use in calibration. 

741 

742 Returns 

743 ------- 

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

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

746 photoCalib. 

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

748 Reference/stars matches used in the fit. 

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

750 Photometric calibration that was fit to the star catalog. 

751 """ 

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

753 calibrated_stars = result.photoCalib.calibrateCatalog(stars) 

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

755 identity = afwImage.PhotoCalib(1.0, 

756 result.photoCalib.getCalibrationErr(), 

757 bbox=exposure.getBBox()) 

758 exposure.setPhotoCalib(identity) 

759 

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

761 

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

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

764 calibrations attached to it. 

765 

766 Parameters 

767 ---------- 

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

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

770 Modified to contain the computed summary statistics. 

771 stars : `SourceCatalog` 

772 Good stars selected used in calibration. 

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

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

775 above stars. 

776 """ 

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

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

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

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

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

782 exposure.info.setSummaryStats(summary)