Coverage for python/lsst/pipe/tasks/calibrate.py: 22%

277 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2022-09-13 10:31 +0000

1# 

2# LSST Data Management System 

3# Copyright 2008-2016 AURA/LSST. 

4# 

5# This product includes software developed by the 

6# LSST Project (http://www.lsst.org/). 

7# 

8# This program is free software: you can redistribute it and/or modify 

9# it under the terms of the GNU General Public License as published by 

10# the Free Software Foundation, either version 3 of the License, or 

11# (at your option) any later version. 

12# 

13# This program is distributed in the hope that it will be useful, 

14# but WITHOUT ANY WARRANTY; without even the implied warranty of 

15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

16# GNU General Public License for more details. 

17# 

18# You should have received a copy of the LSST License Statement and 

19# the GNU General Public License along with this program. If not, 

20# see <https://www.lsstcorp.org/LegalNotices/>. 

21# 

22import math 

23import warnings 

24 

25from lsstDebug import getDebugFrame 

26import lsst.pex.config as pexConfig 

27import lsst.pipe.base as pipeBase 

28import lsst.pipe.base.connectionTypes as cT 

29import lsst.afw.table as afwTable 

30from lsst.meas.astrom import AstrometryTask, displayAstrometry, denormalizeMatches 

31from lsst.meas.algorithms import LoadReferenceObjectsConfig, SkyObjectsTask 

32from lsst.obs.base import ExposureIdInfo 

33import lsst.daf.base as dafBase 

34from lsst.afw.math import BackgroundList 

35from lsst.afw.table import SourceTable 

36from lsst.meas.algorithms import SourceDetectionTask, ReferenceObjectLoader 

37from lsst.meas.base import (SingleFrameMeasurementTask, 

38 ApplyApCorrTask, 

39 CatalogCalculationTask) 

40from lsst.meas.deblender import SourceDeblendTask 

41from lsst.utils.timer import timeMethod 

42from lsst.pipe.tasks.setPrimaryFlags import SetPrimaryFlagsTask 

43from .fakes import BaseFakeSourcesTask 

44from .photoCal import PhotoCalTask 

45from .computeExposureSummaryStats import ComputeExposureSummaryStatsTask 

46 

47 

48__all__ = ["CalibrateConfig", "CalibrateTask"] 

49 

50 

51class CalibrateConnections(pipeBase.PipelineTaskConnections, dimensions=("instrument", "visit", "detector"), 

52 defaultTemplates={}): 

53 

54 icSourceSchema = cT.InitInput( 

55 doc="Schema produced by characterize image task, used to initialize this task", 

56 name="icSrc_schema", 

57 storageClass="SourceCatalog", 

58 ) 

59 

60 outputSchema = cT.InitOutput( 

61 doc="Schema after CalibrateTask has been initialized", 

62 name="src_schema", 

63 storageClass="SourceCatalog", 

64 ) 

65 

66 exposure = cT.Input( 

67 doc="Input image to calibrate", 

68 name="icExp", 

69 storageClass="ExposureF", 

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

71 ) 

72 

73 background = cT.Input( 

74 doc="Backgrounds determined by characterize task", 

75 name="icExpBackground", 

76 storageClass="Background", 

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

78 ) 

79 

80 icSourceCat = cT.Input( 

81 doc="Source catalog created by characterize task", 

82 name="icSrc", 

83 storageClass="SourceCatalog", 

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

85 ) 

86 

87 astromRefCat = cT.PrerequisiteInput( 

88 doc="Reference catalog to use for astrometry", 

89 name="gaia_dr2_20200414", 

90 storageClass="SimpleCatalog", 

91 dimensions=("skypix",), 

92 deferLoad=True, 

93 multiple=True, 

94 ) 

95 

96 photoRefCat = cT.PrerequisiteInput( 

97 doc="Reference catalog to use for photometric calibration", 

98 name="ps1_pv3_3pi_20170110", 

99 storageClass="SimpleCatalog", 

100 dimensions=("skypix",), 

101 deferLoad=True, 

102 multiple=True 

103 ) 

104 

105 outputExposure = cT.Output( 

106 doc="Exposure after running calibration task", 

107 name="calexp", 

108 storageClass="ExposureF", 

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

110 ) 

111 

112 outputCat = cT.Output( 

113 doc="Source catalog produced in calibrate task", 

114 name="src", 

115 storageClass="SourceCatalog", 

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

117 ) 

118 

119 outputBackground = cT.Output( 

120 doc="Background models estimated in calibration task", 

121 name="calexpBackground", 

122 storageClass="Background", 

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

124 ) 

125 

126 matches = cT.Output( 

127 doc="Source/refObj matches from the astrometry solver", 

128 name="srcMatch", 

129 storageClass="Catalog", 

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

131 ) 

132 

133 matchesDenormalized = cT.Output( 

134 doc="Denormalized matches from astrometry solver", 

135 name="srcMatchFull", 

136 storageClass="Catalog", 

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

138 ) 

139 

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

141 super().__init__(config=config) 

142 

143 if config.doAstrometry is False: 

144 self.prerequisiteInputs.remove("astromRefCat") 

145 if config.doPhotoCal is False: 

146 self.prerequisiteInputs.remove("photoRefCat") 

147 

148 if config.doWriteMatches is False or config.doAstrometry is False: 

149 self.outputs.remove("matches") 

150 if config.doWriteMatchesDenormalized is False or config.doAstrometry is False: 

151 self.outputs.remove("matchesDenormalized") 

152 

153 

154class CalibrateConfig(pipeBase.PipelineTaskConfig, pipelineConnections=CalibrateConnections): 

155 """Config for CalibrateTask""" 

156 doWrite = pexConfig.Field( 

157 dtype=bool, 

158 default=True, 

159 doc="Save calibration results?", 

160 ) 

161 doWriteHeavyFootprintsInSources = pexConfig.Field( 

162 dtype=bool, 

163 default=True, 

164 doc="Include HeavyFootprint data in source table? If false then heavy " 

165 "footprints are saved as normal footprints, which saves some space" 

166 ) 

167 doWriteMatches = pexConfig.Field( 

168 dtype=bool, 

169 default=True, 

170 doc="Write reference matches (ignored if doWrite or doAstrometry false)?", 

171 ) 

172 doWriteMatchesDenormalized = pexConfig.Field( 

173 dtype=bool, 

174 default=False, 

175 doc=("Write reference matches in denormalized format? " 

176 "This format uses more disk space, but is more convenient to " 

177 "read. Ignored if doWriteMatches=False or doWrite=False."), 

178 ) 

179 doAstrometry = pexConfig.Field( 

180 dtype=bool, 

181 default=True, 

182 doc="Perform astrometric calibration?", 

183 ) 

184 astromRefObjLoader = pexConfig.ConfigField( 

185 dtype=LoadReferenceObjectsConfig, 

186 doc="reference object loader for astrometric calibration", 

187 ) 

188 photoRefObjLoader = pexConfig.ConfigField( 

189 dtype=LoadReferenceObjectsConfig, 

190 doc="reference object loader for photometric calibration", 

191 ) 

192 astrometry = pexConfig.ConfigurableField( 

193 target=AstrometryTask, 

194 doc="Perform astrometric calibration to refine the WCS", 

195 ) 

196 requireAstrometry = pexConfig.Field( 

197 dtype=bool, 

198 default=True, 

199 doc=("Raise an exception if astrometry fails? Ignored if doAstrometry " 

200 "false."), 

201 ) 

202 doPhotoCal = pexConfig.Field( 

203 dtype=bool, 

204 default=True, 

205 doc="Perform phometric calibration?", 

206 ) 

207 requirePhotoCal = pexConfig.Field( 

208 dtype=bool, 

209 default=True, 

210 doc=("Raise an exception if photoCal fails? Ignored if doPhotoCal " 

211 "false."), 

212 ) 

213 photoCal = pexConfig.ConfigurableField( 

214 target=PhotoCalTask, 

215 doc="Perform photometric calibration", 

216 ) 

217 icSourceFieldsToCopy = pexConfig.ListField( 

218 dtype=str, 

219 default=("calib_psf_candidate", "calib_psf_used", "calib_psf_reserved"), 

220 doc=("Fields to copy from the icSource catalog to the output catalog " 

221 "for matching sources Any missing fields will trigger a " 

222 "RuntimeError exception. Ignored if icSourceCat is not provided.") 

223 ) 

224 matchRadiusPix = pexConfig.Field( 

225 dtype=float, 

226 default=3, 

227 doc=("Match radius for matching icSourceCat objects to sourceCat " 

228 "objects (pixels)"), 

229 ) 

230 checkUnitsParseStrict = pexConfig.Field( 

231 doc=("Strictness of Astropy unit compatibility check, can be 'raise', " 

232 "'warn' or 'silent'"), 

233 dtype=str, 

234 default="raise", 

235 ) 

236 detection = pexConfig.ConfigurableField( 

237 target=SourceDetectionTask, 

238 doc="Detect sources" 

239 ) 

240 doDeblend = pexConfig.Field( 

241 dtype=bool, 

242 default=True, 

243 doc="Run deblender input exposure" 

244 ) 

245 deblend = pexConfig.ConfigurableField( 

246 target=SourceDeblendTask, 

247 doc="Split blended sources into their components" 

248 ) 

249 doSkySources = pexConfig.Field( 

250 dtype=bool, 

251 default=True, 

252 doc="Generate sky sources?", 

253 ) 

254 skySources = pexConfig.ConfigurableField( 

255 target=SkyObjectsTask, 

256 doc="Generate sky sources", 

257 ) 

258 measurement = pexConfig.ConfigurableField( 

259 target=SingleFrameMeasurementTask, 

260 doc="Measure sources" 

261 ) 

262 postCalibrationMeasurement = pexConfig.ConfigurableField( 

263 target=SingleFrameMeasurementTask, 

264 doc="Second round of measurement for plugins that need to be run after photocal" 

265 ) 

266 setPrimaryFlags = pexConfig.ConfigurableField( 

267 target=SetPrimaryFlagsTask, 

268 doc=("Set flags for primary source classification in single frame " 

269 "processing. True if sources are not sky sources and not a parent.") 

270 ) 

271 doApCorr = pexConfig.Field( 

272 dtype=bool, 

273 default=True, 

274 doc="Run subtask to apply aperture correction" 

275 ) 

276 applyApCorr = pexConfig.ConfigurableField( 

277 target=ApplyApCorrTask, 

278 doc="Subtask to apply aperture corrections" 

279 ) 

280 # If doApCorr is False, and the exposure does not have apcorrections 

281 # already applied, the active plugins in catalogCalculation almost 

282 # certainly should not contain the characterization plugin 

283 catalogCalculation = pexConfig.ConfigurableField( 

284 target=CatalogCalculationTask, 

285 doc="Subtask to run catalogCalculation plugins on catalog" 

286 ) 

287 doInsertFakes = pexConfig.Field( 

288 dtype=bool, 

289 default=False, 

290 doc="Run fake sources injection task", 

291 deprecated=("doInsertFakes is no longer supported. This config will be removed after v24. " 

292 "Please use ProcessCcdWithFakesTask instead.") 

293 ) 

294 insertFakes = pexConfig.ConfigurableField( 

295 target=BaseFakeSourcesTask, 

296 doc="Injection of fake sources for testing purposes (must be " 

297 "retargeted)", 

298 deprecated=("insertFakes is no longer supported. This config will be removed after v24. " 

299 "Please use ProcessCcdWithFakesTask instead.") 

300 ) 

301 doComputeSummaryStats = pexConfig.Field( 

302 dtype=bool, 

303 default=True, 

304 doc="Run subtask to measure exposure summary statistics?" 

305 ) 

306 computeSummaryStats = pexConfig.ConfigurableField( 

307 target=ComputeExposureSummaryStatsTask, 

308 doc="Subtask to run computeSummaryStats on exposure" 

309 ) 

310 doWriteExposure = pexConfig.Field( 

311 dtype=bool, 

312 default=True, 

313 doc="Write the calexp? If fakes have been added then we do not want to write out the calexp as a " 

314 "normal calexp but as a fakes_calexp." 

315 ) 

316 

317 def setDefaults(self): 

318 super().setDefaults() 

319 self.detection.doTempLocalBackground = False 

320 self.deblend.maxFootprintSize = 2000 

321 self.postCalibrationMeasurement.plugins.names = ["base_LocalPhotoCalib", "base_LocalWcs"] 

322 self.postCalibrationMeasurement.doReplaceWithNoise = False 

323 for key in self.postCalibrationMeasurement.slots: 

324 setattr(self.postCalibrationMeasurement.slots, key, None) 

325 self.astromRefObjLoader.anyFilterMapsToThis = "phot_g_mean" 

326 # The photoRefCat connection is the name to use for the colorterms. 

327 self.photoCal.photoCatName = self.connections.photoRefCat 

328 

329 

330class CalibrateTask(pipeBase.PipelineTask): 

331 """Task to calibrate an exposure. 

332 

333 Parameters 

334 ---------- 

335 butler : `None` 

336 Compatibility parameter. Should always be `None`. 

337 astromRefObjLoader : `lsst.meas.algorithms.ReferenceObjectLoader`, optional 

338 Reference object loader for astrometry task. Must be None if 

339 run as part of PipelineTask. 

340 photoRefObjLoader : `lsst.meas.algorithms.ReferenceObjectLoader`, optional 

341 Reference object loader for photometry task. Must be None if 

342 run as part of PipelineTask. 

343 icSourceSchema : `lsst.afw.table.Schema`, optional 

344 Schema for the icSource catalog. 

345 initInputs : `dict`, optional 

346 Dictionary that can contain a key ``icSourceSchema`` containing the 

347 input schema. If present will override the value of ``icSourceSchema``. 

348 """ 

349 ConfigClass = CalibrateConfig 

350 _DefaultName = "calibrate" 

351 

352 def __init__(self, butler=None, astromRefObjLoader=None, 

353 photoRefObjLoader=None, icSourceSchema=None, 

354 initInputs=None, **kwargs): 

355 super().__init__(**kwargs) 

356 

357 if butler is not None: 

358 warnings.warn("The 'butler' parameter is no longer used and can be safely removed.", 

359 category=FutureWarning, stacklevel=2) 

360 butler = None 

361 

362 if initInputs is not None: 

363 icSourceSchema = initInputs['icSourceSchema'].schema 

364 

365 if icSourceSchema is not None: 

366 # use a schema mapper to avoid copying each field separately 

367 self.schemaMapper = afwTable.SchemaMapper(icSourceSchema) 

368 minimumSchema = afwTable.SourceTable.makeMinimalSchema() 

369 self.schemaMapper.addMinimalSchema(minimumSchema, False) 

370 

371 # Add fields to copy from an icSource catalog 

372 # and a field to indicate that the source matched a source in that 

373 # catalog. If any fields are missing then raise an exception, but 

374 # first find all missing fields in order to make the error message 

375 # more useful. 

376 self.calibSourceKey = self.schemaMapper.addOutputField( 

377 afwTable.Field["Flag"]("calib_detected", 

378 "Source was detected as an icSource")) 

379 missingFieldNames = [] 

380 for fieldName in self.config.icSourceFieldsToCopy: 

381 try: 

382 schemaItem = icSourceSchema.find(fieldName) 

383 except Exception: 

384 missingFieldNames.append(fieldName) 

385 else: 

386 # field found; if addMapping fails then raise an exception 

387 self.schemaMapper.addMapping(schemaItem.getKey()) 

388 

389 if missingFieldNames: 

390 raise RuntimeError("isSourceCat is missing fields {} " 

391 "specified in icSourceFieldsToCopy" 

392 .format(missingFieldNames)) 

393 

394 # produce a temporary schema to pass to the subtasks; finalize it 

395 # later 

396 self.schema = self.schemaMapper.editOutputSchema() 

397 else: 

398 self.schemaMapper = None 

399 self.schema = afwTable.SourceTable.makeMinimalSchema() 

400 self.makeSubtask('detection', schema=self.schema) 

401 

402 self.algMetadata = dafBase.PropertyList() 

403 

404 if self.config.doDeblend: 

405 self.makeSubtask("deblend", schema=self.schema) 

406 if self.config.doSkySources: 

407 self.makeSubtask("skySources") 

408 self.skySourceKey = self.schema.addField("sky_source", type="Flag", doc="Sky objects.") 

409 self.makeSubtask('measurement', schema=self.schema, 

410 algMetadata=self.algMetadata) 

411 self.makeSubtask('postCalibrationMeasurement', schema=self.schema, 

412 algMetadata=self.algMetadata) 

413 self.makeSubtask("setPrimaryFlags", schema=self.schema, isSingleFrame=True) 

414 if self.config.doApCorr: 

415 self.makeSubtask('applyApCorr', schema=self.schema) 

416 self.makeSubtask('catalogCalculation', schema=self.schema) 

417 

418 if self.config.doAstrometry: 

419 self.makeSubtask("astrometry", refObjLoader=astromRefObjLoader, 

420 schema=self.schema) 

421 if self.config.doPhotoCal: 

422 self.makeSubtask("photoCal", refObjLoader=photoRefObjLoader, 

423 schema=self.schema) 

424 if self.config.doComputeSummaryStats: 

425 self.makeSubtask('computeSummaryStats') 

426 

427 if initInputs is not None and (astromRefObjLoader is not None or photoRefObjLoader is not None): 

428 raise RuntimeError("PipelineTask form of this task should not be initialized with " 

429 "reference object loaders.") 

430 

431 if self.schemaMapper is not None: 

432 # finalize the schema 

433 self.schema = self.schemaMapper.getOutputSchema() 

434 self.schema.checkUnits(parse_strict=self.config.checkUnitsParseStrict) 

435 

436 sourceCatSchema = afwTable.SourceCatalog(self.schema) 

437 sourceCatSchema.getTable().setMetadata(self.algMetadata) 

438 self.outputSchema = sourceCatSchema 

439 

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

441 inputs = butlerQC.get(inputRefs) 

442 inputs['exposureIdInfo'] = ExposureIdInfo.fromDataId(butlerQC.quantum.dataId, "visit_detector") 

443 

444 if self.config.doAstrometry: 

445 refObjLoader = ReferenceObjectLoader(dataIds=[ref.datasetRef.dataId 

446 for ref in inputRefs.astromRefCat], 

447 refCats=inputs.pop('astromRefCat'), 

448 name=self.config.connections.astromRefCat, 

449 config=self.config.astromRefObjLoader, log=self.log) 

450 self.astrometry.setRefObjLoader(refObjLoader) 

451 

452 if self.config.doPhotoCal: 

453 photoRefObjLoader = ReferenceObjectLoader(dataIds=[ref.datasetRef.dataId 

454 for ref in inputRefs.photoRefCat], 

455 refCats=inputs.pop('photoRefCat'), 

456 name=self.config.connections.photoRefCat, 

457 config=self.config.photoRefObjLoader, 

458 log=self.log) 

459 self.photoCal.match.setRefObjLoader(photoRefObjLoader) 

460 

461 outputs = self.run(**inputs) 

462 

463 if self.config.doWriteMatches and self.config.doAstrometry: 

464 normalizedMatches = afwTable.packMatches(outputs.astromMatches) 

465 normalizedMatches.table.setMetadata(outputs.matchMeta) 

466 if self.config.doWriteMatchesDenormalized: 

467 denormMatches = denormalizeMatches(outputs.astromMatches, outputs.matchMeta) 

468 outputs.matchesDenormalized = denormMatches 

469 outputs.matches = normalizedMatches 

470 butlerQC.put(outputs, outputRefs) 

471 

472 @timeMethod 

473 def run(self, exposure, exposureIdInfo=None, background=None, 

474 icSourceCat=None): 

475 """Calibrate an exposure. 

476 

477 Parameters 

478 ---------- 

479 exposure : `lsst.afw.image.ExposureF` 

480 Exposure to calibrate. 

481 exposureIdInfo : `lsst.obs.baseExposureIdInfo`, optional 

482 Exposure ID info. If not provided, returned SourceCatalog IDs will not 

483 be globally unique. 

484 background : `lsst.afw.math.BackgroundList`, optional 

485 Initial model of background already subtracted from exposure. 

486 icSourceCat : `lsst.afw.image.SourceCatalog`, optional 

487 SourceCatalog from CharacterizeImageTask from which we can copy some fields. 

488 

489 Returns 

490 ------- 

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

492 Result structure with the following attributes: 

493 

494 ``exposure`` 

495 Characterized exposure (`lsst.afw.image.ExposureF`). 

496 ``sourceCat`` 

497 Detected sources (`lsst.afw.table.SourceCatalog`). 

498 ``outputBackground`` 

499 Model of subtracted background (`lsst.afw.math.BackgroundList`). 

500 ``astromMatches`` 

501 List of source/ref matches from astrometry solver. 

502 ``matchMeta`` 

503 Metadata from astrometry matches. 

504 ``outputExposure`` 

505 Another reference to ``exposure`` for compatibility. 

506 ``outputCat`` 

507 Another reference to ``sourceCat`` for compatibility. 

508 """ 

509 # detect, deblend and measure sources 

510 if exposureIdInfo is None: 

511 exposureIdInfo = ExposureIdInfo() 

512 

513 if background is None: 

514 background = BackgroundList() 

515 sourceIdFactory = exposureIdInfo.makeSourceIdFactory() 

516 table = SourceTable.make(self.schema, sourceIdFactory) 

517 table.setMetadata(self.algMetadata) 

518 

519 detRes = self.detection.run(table=table, exposure=exposure, 

520 doSmooth=True) 

521 sourceCat = detRes.sources 

522 if detRes.fpSets.background: 

523 for bg in detRes.fpSets.background: 

524 background.append(bg) 

525 if self.config.doSkySources: 

526 skySourceFootprints = self.skySources.run(mask=exposure.mask, seed=exposureIdInfo.expId) 

527 if skySourceFootprints: 

528 for foot in skySourceFootprints: 

529 s = sourceCat.addNew() 

530 s.setFootprint(foot) 

531 s.set(self.skySourceKey, True) 

532 if self.config.doDeblend: 

533 self.deblend.run(exposure=exposure, sources=sourceCat) 

534 self.measurement.run( 

535 measCat=sourceCat, 

536 exposure=exposure, 

537 exposureId=exposureIdInfo.expId 

538 ) 

539 if self.config.doApCorr: 

540 self.applyApCorr.run( 

541 catalog=sourceCat, 

542 apCorrMap=exposure.getInfo().getApCorrMap() 

543 ) 

544 self.catalogCalculation.run(sourceCat) 

545 

546 self.setPrimaryFlags.run(sourceCat) 

547 

548 if icSourceCat is not None and \ 

549 len(self.config.icSourceFieldsToCopy) > 0: 

550 self.copyIcSourceFields(icSourceCat=icSourceCat, 

551 sourceCat=sourceCat) 

552 

553 # TODO DM-11568: this contiguous check-and-copy could go away if we 

554 # reserve enough space during SourceDetection and/or SourceDeblend. 

555 # NOTE: sourceSelectors require contiguous catalogs, so ensure 

556 # contiguity now, so views are preserved from here on. 

557 if not sourceCat.isContiguous(): 

558 sourceCat = sourceCat.copy(deep=True) 

559 

560 # perform astrometry calibration: 

561 # fit an improved WCS and update the exposure's WCS in place 

562 astromMatches = None 

563 matchMeta = None 

564 if self.config.doAstrometry: 

565 try: 

566 astromRes = self.astrometry.run( 

567 exposure=exposure, 

568 sourceCat=sourceCat, 

569 ) 

570 astromMatches = astromRes.matches 

571 matchMeta = astromRes.matchMeta 

572 except Exception as e: 

573 if self.config.requireAstrometry: 

574 raise 

575 self.log.warning("Unable to perform astrometric calibration " 

576 "(%s): attempting to proceed", e) 

577 

578 # compute photometric calibration 

579 if self.config.doPhotoCal: 

580 try: 

581 photoRes = self.photoCal.run(exposure, sourceCat=sourceCat, expId=exposureIdInfo.expId) 

582 exposure.setPhotoCalib(photoRes.photoCalib) 

583 # TODO: reword this to phrase it in terms of the calibration factor? 

584 self.log.info("Photometric zero-point: %f", 

585 photoRes.photoCalib.instFluxToMagnitude(1.0)) 

586 self.setMetadata(exposure=exposure, photoRes=photoRes) 

587 except Exception as e: 

588 if self.config.requirePhotoCal: 

589 raise 

590 self.log.warning("Unable to perform photometric calibration " 

591 "(%s): attempting to proceed", e) 

592 self.setMetadata(exposure=exposure, photoRes=None) 

593 

594 self.postCalibrationMeasurement.run( 

595 measCat=sourceCat, 

596 exposure=exposure, 

597 exposureId=exposureIdInfo.expId 

598 ) 

599 

600 if self.config.doComputeSummaryStats: 

601 summary = self.computeSummaryStats.run(exposure=exposure, 

602 sources=sourceCat, 

603 background=background) 

604 exposure.getInfo().setSummaryStats(summary) 

605 

606 frame = getDebugFrame(self._display, "calibrate") 

607 if frame: 

608 displayAstrometry( 

609 sourceCat=sourceCat, 

610 exposure=exposure, 

611 matches=astromMatches, 

612 frame=frame, 

613 pause=False, 

614 ) 

615 

616 return pipeBase.Struct( 

617 sourceCat=sourceCat, 

618 astromMatches=astromMatches, 

619 matchMeta=matchMeta, 

620 outputExposure=exposure, 

621 outputCat=sourceCat, 

622 outputBackground=background, 

623 ) 

624 

625 def getSchemaCatalogs(self): 

626 """Return a dict of empty catalogs for each catalog dataset produced 

627 by this task. 

628 """ 

629 sourceCat = afwTable.SourceCatalog(self.schema) 

630 sourceCat.getTable().setMetadata(self.algMetadata) 

631 return {"src": sourceCat} 

632 

633 def setMetadata(self, exposure, photoRes=None): 

634 """Set task and exposure metadata. 

635 

636 Logs a warning continues if needed data is missing. 

637 

638 Parameters 

639 ---------- 

640 exposure : `lsst.afw.image.ExposureF` 

641 Exposure to set metadata on. 

642 photoRes : `lsst.pipe.base.Struct`, optional 

643 Result of running photoCal task. 

644 """ 

645 if photoRes is None: 

646 return 

647 

648 metadata = exposure.getMetadata() 

649 

650 # convert zero-point to (mag/sec/adu) for task MAGZERO metadata 

651 try: 

652 exposureTime = exposure.getInfo().getVisitInfo().getExposureTime() 

653 magZero = photoRes.zp - 2.5*math.log10(exposureTime) 

654 except Exception: 

655 self.log.warning("Could not set normalized MAGZERO in header: no " 

656 "exposure time") 

657 magZero = math.nan 

658 

659 try: 

660 metadata.set('MAGZERO', magZero) 

661 metadata.set('MAGZERO_RMS', photoRes.sigma) 

662 metadata.set('MAGZERO_NOBJ', photoRes.ngood) 

663 metadata.set('COLORTERM1', 0.0) 

664 metadata.set('COLORTERM2', 0.0) 

665 metadata.set('COLORTERM3', 0.0) 

666 except Exception as e: 

667 self.log.warning("Could not set exposure metadata: %s", e) 

668 

669 def copyIcSourceFields(self, icSourceCat, sourceCat): 

670 """Match sources in an icSourceCat and a sourceCat and copy fields. 

671 

672 The fields copied are those specified by ``config.icSourceFieldsToCopy``. 

673 

674 Parameters 

675 ---------- 

676 icSourceCat : `lsst.afw.table.SourceCatalog` 

677 Catalog from which to copy fields. 

678 sourceCat : `lsst.afw.table.SourceCatalog` 

679 Catalog to which to copy fields. 

680 """ 

681 if self.schemaMapper is None: 

682 raise RuntimeError("To copy icSource fields you must specify " 

683 "icSourceSchema nd icSourceKeys when " 

684 "constructing this task") 

685 if icSourceCat is None or sourceCat is None: 

686 raise RuntimeError("icSourceCat and sourceCat must both be " 

687 "specified") 

688 if len(self.config.icSourceFieldsToCopy) == 0: 

689 self.log.warning("copyIcSourceFields doing nothing because " 

690 "icSourceFieldsToCopy is empty") 

691 return 

692 

693 mc = afwTable.MatchControl() 

694 mc.findOnlyClosest = False # return all matched objects 

695 matches = afwTable.matchXy(icSourceCat, sourceCat, 

696 self.config.matchRadiusPix, mc) 

697 if self.config.doDeblend: 

698 deblendKey = sourceCat.schema["deblend_nChild"].asKey() 

699 # if deblended, keep children 

700 matches = [m for m in matches if m[1].get(deblendKey) == 0] 

701 

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

703 # need to prune to the best matches 

704 # closest matches as a dict of icSourceCat source ID: 

705 # (icSourceCat source, sourceCat source, distance in pixels) 

706 bestMatches = {} 

707 for m0, m1, d in matches: 

708 id0 = m0.getId() 

709 match = bestMatches.get(id0) 

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

711 bestMatches[id0] = (m0, m1, d) 

712 matches = list(bestMatches.values()) 

713 

714 # Check that no sourceCat sources are listed twice (we already know 

715 # that each match has a unique icSourceCat source ID, due to using 

716 # that ID as the key in bestMatches) 

717 numMatches = len(matches) 

718 numUniqueSources = len(set(m[1].getId() for m in matches)) 

719 if numUniqueSources != numMatches: 

720 self.log.warning("%d icSourceCat sources matched only %d sourceCat " 

721 "sources", numMatches, numUniqueSources) 

722 

723 self.log.info("Copying flags from icSourceCat to sourceCat for " 

724 "%d sources", numMatches) 

725 

726 # For each match: set the calibSourceKey flag and copy the desired 

727 # fields 

728 for icSrc, src, d in matches: 

729 src.setFlag(self.calibSourceKey, True) 

730 # src.assign copies the footprint from icSrc, which we don't want 

731 # (DM-407) 

732 # so set icSrc's footprint to src's footprint before src.assign, 

733 # then restore it 

734 icSrcFootprint = icSrc.getFootprint() 

735 try: 

736 icSrc.setFootprint(src.getFootprint()) 

737 src.assign(icSrc, self.schemaMapper) 

738 finally: 

739 icSrc.setFootprint(icSrcFootprint)