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

281 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 

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

23 

24import math 

25import numpy as np 

26 

27from lsstDebug import getDebugFrame 

28import lsst.pex.config as pexConfig 

29import lsst.pipe.base as pipeBase 

30import lsst.pipe.base.connectionTypes as cT 

31import lsst.afw.table as afwTable 

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

33from lsst.meas.algorithms import LoadReferenceObjectsConfig, SkyObjectsTask 

34import lsst.daf.base as dafBase 

35from lsst.afw.math import BackgroundList 

36from lsst.afw.table import SourceTable 

37from lsst.meas.algorithms import SourceDetectionTask, ReferenceObjectLoader 

38from lsst.meas.base import (SingleFrameMeasurementTask, 

39 ApplyApCorrTask, 

40 CatalogCalculationTask, 

41 IdGenerator, 

42 DetectorVisitIdGeneratorConfig) 

43from lsst.meas.deblender import SourceDeblendTask 

44from lsst.utils.timer import timeMethod 

45from lsst.pipe.tasks.setPrimaryFlags import SetPrimaryFlagsTask 

46from .photoCal import PhotoCalTask 

47from .computeExposureSummaryStats import ComputeExposureSummaryStatsTask 

48 

49 

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

51 defaultTemplates={}): 

52 

53 icSourceSchema = cT.InitInput( 

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

55 name="icSrc_schema", 

56 storageClass="SourceCatalog", 

57 ) 

58 

59 outputSchema = cT.InitOutput( 

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

61 name="src_schema", 

62 storageClass="SourceCatalog", 

63 ) 

64 

65 exposure = cT.Input( 

66 doc="Input image to calibrate", 

67 name="icExp", 

68 storageClass="ExposureF", 

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

70 ) 

71 

72 background = cT.Input( 

73 doc="Backgrounds determined by characterize task", 

74 name="icExpBackground", 

75 storageClass="Background", 

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

77 ) 

78 

79 icSourceCat = cT.Input( 

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

81 name="icSrc", 

82 storageClass="SourceCatalog", 

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

84 ) 

85 

86 astromRefCat = cT.PrerequisiteInput( 

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

88 name="gaia_dr3_20230707", 

89 storageClass="SimpleCatalog", 

90 dimensions=("skypix",), 

91 deferLoad=True, 

92 multiple=True, 

93 ) 

94 

95 photoRefCat = cT.PrerequisiteInput( 

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

97 name="ps1_pv3_3pi_20170110", 

98 storageClass="SimpleCatalog", 

99 dimensions=("skypix",), 

100 deferLoad=True, 

101 multiple=True 

102 ) 

103 

104 outputExposure = cT.Output( 

105 doc="Exposure after running calibration task", 

106 name="calexp", 

107 storageClass="ExposureF", 

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

109 ) 

110 

111 outputCat = cT.Output( 

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

113 name="src", 

114 storageClass="SourceCatalog", 

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

116 ) 

117 

118 outputBackground = cT.Output( 

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

120 name="calexpBackground", 

121 storageClass="Background", 

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

123 ) 

124 

125 matches = cT.Output( 

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

127 name="srcMatch", 

128 storageClass="Catalog", 

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

130 ) 

131 

132 matchesDenormalized = cT.Output( 

133 doc="Denormalized matches from astrometry solver", 

134 name="srcMatchFull", 

135 storageClass="Catalog", 

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

137 ) 

138 

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

140 super().__init__(config=config) 

141 

142 if config.doAstrometry is False: 

143 self.prerequisiteInputs.remove("astromRefCat") 

144 if config.doPhotoCal is False: 

145 self.prerequisiteInputs.remove("photoRefCat") 

146 

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

148 self.outputs.remove("matches") 

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

150 self.outputs.remove("matchesDenormalized") 

151 

152 

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

154 """Config for CalibrateTask.""" 

155 

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 doComputeSummaryStats = pexConfig.Field( 

288 dtype=bool, 

289 default=True, 

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

291 ) 

292 computeSummaryStats = pexConfig.ConfigurableField( 

293 target=ComputeExposureSummaryStatsTask, 

294 doc="Subtask to run computeSummaryStats on exposure" 

295 ) 

296 doWriteExposure = pexConfig.Field( 

297 dtype=bool, 

298 default=True, 

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

300 "normal calexp but as a fakes_calexp." 

301 ) 

302 idGenerator = DetectorVisitIdGeneratorConfig.make_field() 

303 

304 def setDefaults(self): 

305 super().setDefaults() 

306 self.detection.doTempLocalBackground = False 

307 self.deblend.maxFootprintSize = 2000 

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

309 self.postCalibrationMeasurement.doReplaceWithNoise = False 

310 for key in self.postCalibrationMeasurement.slots: 

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

312 self.astromRefObjLoader.anyFilterMapsToThis = "phot_g_mean" 

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

314 self.photoCal.photoCatName = self.connections.photoRefCat 

315 

316 

317class CalibrateTask(pipeBase.PipelineTask): 

318 """Calibrate an exposure: measure sources and perform astrometric and 

319 photometric calibration. 

320 

321 Given an exposure with a good PSF model and aperture correction map(e.g. as 

322 provided by `~lsst.pipe.tasks.characterizeImage.CharacterizeImageTask`), 

323 perform the following operations: 

324 - Run detection and measurement 

325 - Run astrometry subtask to fit an improved WCS 

326 - Run photoCal subtask to fit the exposure's photometric zero-point 

327 

328 Parameters 

329 ---------- 

330 butler : `None` 

331 Compatibility parameter. Should always be `None`. 

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

333 Unused in gen3: must be `None`. 

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

335 Unused in gen3: must be `None`. 

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

337 Schema for the icSource catalog. 

338 initInputs : `dict`, optional 

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

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

341 

342 Raises 

343 ------ 

344 RuntimeError 

345 Raised if any of the following occur: 

346 - isSourceCat is missing fields specified in icSourceFieldsToCopy. 

347 - PipelineTask form of this task is initialized with reference object 

348 loaders. 

349 

350 Notes 

351 ----- 

352 Quantities set in exposure Metadata: 

353 

354 MAGZERO_RMS 

355 MAGZERO's RMS == sigma reported by photoCal task 

356 MAGZERO_NOBJ 

357 Number of stars used == ngood reported by photoCal task 

358 COLORTERM1 

359 ?? (always 0.0) 

360 COLORTERM2 

361 ?? (always 0.0) 

362 COLORTERM3 

363 ?? (always 0.0) 

364 

365 Debugging: 

366 CalibrateTask has a debug dictionary containing one key: 

367 

368 calibrate 

369 frame (an int; <= 0 to not display) in which to display the exposure, 

370 sources and matches. See @ref lsst.meas.astrom.displayAstrometry for 

371 the meaning of the various symbols. 

372 """ 

373 

374 ConfigClass = CalibrateConfig 

375 _DefaultName = "calibrate" 

376 

377 def __init__(self, astromRefObjLoader=None, 

378 photoRefObjLoader=None, icSourceSchema=None, 

379 initInputs=None, **kwargs): 

380 super().__init__(**kwargs) 

381 

382 if initInputs is not None: 

383 icSourceSchema = initInputs['icSourceSchema'].schema 

384 

385 if icSourceSchema is not None: 

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

387 self.schemaMapper = afwTable.SchemaMapper(icSourceSchema) 

388 minimumSchema = afwTable.SourceTable.makeMinimalSchema() 

389 self.schemaMapper.addMinimalSchema(minimumSchema, False) 

390 

391 # Add fields to copy from an icSource catalog 

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

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

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

395 # more useful. 

396 self.calibSourceKey = self.schemaMapper.addOutputField( 

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

398 "Source was detected as an icSource")) 

399 missingFieldNames = [] 

400 for fieldName in self.config.icSourceFieldsToCopy: 

401 try: 

402 schemaItem = icSourceSchema.find(fieldName) 

403 except Exception: 

404 missingFieldNames.append(fieldName) 

405 else: 

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

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

408 

409 if missingFieldNames: 

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

411 "specified in icSourceFieldsToCopy" 

412 .format(missingFieldNames)) 

413 

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

415 # later 

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

417 else: 

418 self.schemaMapper = None 

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

420 afwTable.CoordKey.addErrorFields(self.schema) 

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

422 

423 self.algMetadata = dafBase.PropertyList() 

424 

425 if self.config.doDeblend: 

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

427 if self.config.doSkySources: 

428 self.makeSubtask("skySources") 

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

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

431 algMetadata=self.algMetadata) 

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

433 algMetadata=self.algMetadata) 

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

435 if self.config.doApCorr: 

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

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

438 

439 if self.config.doAstrometry: 

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

441 schema=self.schema) 

442 if self.config.doPhotoCal: 

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

444 schema=self.schema) 

445 if self.config.doComputeSummaryStats: 

446 self.makeSubtask('computeSummaryStats') 

447 

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

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

450 "reference object loaders.") 

451 

452 if self.schemaMapper is not None: 

453 # finalize the schema 

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

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

456 

457 sourceCatSchema = afwTable.SourceCatalog(self.schema) 

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

459 self.outputSchema = sourceCatSchema 

460 

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

462 inputs = butlerQC.get(inputRefs) 

463 inputs['idGenerator'] = self.config.idGenerator.apply(butlerQC.quantum.dataId) 

464 

465 if self.config.doAstrometry: 

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

467 for ref in inputRefs.astromRefCat], 

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

469 name=self.config.connections.astromRefCat, 

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

471 self.astrometry.setRefObjLoader(refObjLoader) 

472 

473 if self.config.doPhotoCal: 

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

475 for ref in inputRefs.photoRefCat], 

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

477 name=self.config.connections.photoRefCat, 

478 config=self.config.photoRefObjLoader, 

479 log=self.log) 

480 self.photoCal.match.setRefObjLoader(photoRefObjLoader) 

481 

482 outputs = self.run(**inputs) 

483 

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

485 if outputs.astromMatches is not None: 

486 normalizedMatches = afwTable.packMatches(outputs.astromMatches) 

487 normalizedMatches.table.setMetadata(outputs.matchMeta) 

488 if self.config.doWriteMatchesDenormalized: 

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

490 outputs.matchesDenormalized = denormMatches 

491 outputs.matches = normalizedMatches 

492 else: 

493 del outputRefs.matches 

494 if self.config.doWriteMatchesDenormalized: 

495 del outputRefs.matchesDenormalized 

496 butlerQC.put(outputs, outputRefs) 

497 

498 @timeMethod 

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

500 icSourceCat=None, idGenerator=None): 

501 """Calibrate an exposure. 

502 

503 Parameters 

504 ---------- 

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

506 Exposure to calibrate. 

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

508 Exposure ID info. Deprecated in favor of ``idGenerator``, and 

509 ignored if that is provided. 

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

511 Initial model of background already subtracted from exposure. 

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

513 SourceCatalog from CharacterizeImageTask from which we can copy 

514 some fields. 

515 idGenerator : `lsst.meas.base.IdGenerator`, optional 

516 Object that generates source IDs and provides RNG seeds. 

517 

518 Returns 

519 ------- 

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

521 Results as a struct with attributes: 

522 

523 ``exposure`` 

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

525 ``sourceCat`` 

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

527 ``outputBackground`` 

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

529 ``astromMatches`` 

530 List of source/ref matches from astrometry solver. 

531 ``matchMeta`` 

532 Metadata from astrometry matches. 

533 ``outputExposure`` 

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

535 ``outputCat`` 

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

537 """ 

538 # detect, deblend and measure sources 

539 if idGenerator is None: 

540 if exposureIdInfo is not None: 

541 idGenerator = IdGenerator._from_exposure_id_info(exposureIdInfo) 

542 else: 

543 idGenerator = IdGenerator() 

544 

545 if background is None: 

546 background = BackgroundList() 

547 table = SourceTable.make(self.schema, idGenerator.make_table_id_factory()) 

548 table.setMetadata(self.algMetadata) 

549 

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

551 doSmooth=True) 

552 sourceCat = detRes.sources 

553 if detRes.background: 

554 for bg in detRes.background: 

555 background.append(bg) 

556 if self.config.doSkySources: 

557 skySourceFootprints = self.skySources.run(mask=exposure.mask, seed=idGenerator.catalog_id) 

558 if skySourceFootprints: 

559 for foot in skySourceFootprints: 

560 s = sourceCat.addNew() 

561 s.setFootprint(foot) 

562 s.set(self.skySourceKey, True) 

563 if self.config.doDeblend: 

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

565 self.measurement.run( 

566 measCat=sourceCat, 

567 exposure=exposure, 

568 exposureId=idGenerator.catalog_id, 

569 ) 

570 if self.config.doApCorr: 

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

572 if apCorrMap is None: 

573 self.log.warning("Image does not have valid aperture correction map for %r; " 

574 "skipping aperture correction", idGenerator) 

575 else: 

576 self.applyApCorr.run( 

577 catalog=sourceCat, 

578 apCorrMap=apCorrMap, 

579 ) 

580 self.catalogCalculation.run(sourceCat) 

581 

582 self.setPrimaryFlags.run(sourceCat) 

583 

584 if icSourceCat is not None and \ 

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

586 self.copyIcSourceFields(icSourceCat=icSourceCat, 

587 sourceCat=sourceCat) 

588 

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

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

591 # NOTE: sourceSelectors require contiguous catalogs, so ensure 

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

593 if not sourceCat.isContiguous(): 

594 sourceCat = sourceCat.copy(deep=True) 

595 

596 # perform astrometry calibration: 

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

598 astromMatches = None 

599 matchMeta = None 

600 if self.config.doAstrometry: 

601 astromRes = self.astrometry.run( 

602 exposure=exposure, 

603 sourceCat=sourceCat, 

604 ) 

605 astromMatches = astromRes.matches 

606 matchMeta = astromRes.matchMeta 

607 if exposure.getWcs() is None: 

608 if self.config.requireAstrometry: 

609 raise RuntimeError(f"WCS fit failed for {idGenerator} and requireAstrometry " 

610 "is True.") 

611 else: 

612 self.log.warning("Unable to perform astrometric calibration for %r but " 

613 "requireAstrometry is False: attempting to proceed...", 

614 idGenerator) 

615 

616 # compute photometric calibration 

617 if self.config.doPhotoCal: 

618 if np.all(np.isnan(sourceCat["coord_ra"])) or np.all(np.isnan(sourceCat["coord_dec"])): 

619 if self.config.requirePhotoCal: 

620 raise RuntimeError(f"Astrometry failed for {idGenerator}, so cannot do " 

621 "photoCal, but requirePhotoCal is True.") 

622 self.log.warning("Astrometry failed for %r, so cannot do photoCal. requirePhotoCal " 

623 "is False, so skipping photometric calibration and setting photoCalib " 

624 "to None. Attempting to proceed...", idGenerator) 

625 exposure.setPhotoCalib(None) 

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

627 else: 

628 try: 

629 photoRes = self.photoCal.run( 

630 exposure, sourceCat=sourceCat, expId=idGenerator.catalog_id 

631 ) 

632 exposure.setPhotoCalib(photoRes.photoCalib) 

633 # TODO: reword this to phrase it in terms of the 

634 # calibration factor? 

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

636 photoRes.photoCalib.instFluxToMagnitude(1.0)) 

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

638 except Exception as e: 

639 if self.config.requirePhotoCal: 

640 raise 

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

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

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

644 

645 self.postCalibrationMeasurement.run( 

646 measCat=sourceCat, 

647 exposure=exposure, 

648 exposureId=idGenerator.catalog_id, 

649 ) 

650 

651 if self.config.doComputeSummaryStats: 

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

653 sources=sourceCat, 

654 background=background) 

655 exposure.getInfo().setSummaryStats(summary) 

656 

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

658 if frame: 

659 displayAstrometry( 

660 sourceCat=sourceCat, 

661 exposure=exposure, 

662 matches=astromMatches, 

663 frame=frame, 

664 pause=False, 

665 ) 

666 

667 return pipeBase.Struct( 

668 sourceCat=sourceCat, 

669 astromMatches=astromMatches, 

670 matchMeta=matchMeta, 

671 outputExposure=exposure, 

672 outputCat=sourceCat, 

673 outputBackground=background, 

674 ) 

675 

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

677 """Set task and exposure metadata. 

678 

679 Logs a warning continues if needed data is missing. 

680 

681 Parameters 

682 ---------- 

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

684 Exposure to set metadata on. 

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

686 Result of running photoCal task. 

687 """ 

688 if photoRes is None: 

689 return 

690 

691 metadata = exposure.getMetadata() 

692 

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

694 try: 

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

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

697 except Exception: 

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

699 "exposure time") 

700 magZero = math.nan 

701 

702 try: 

703 metadata.set('MAGZERO', magZero) 

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

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

706 metadata.set('COLORTERM1', 0.0) 

707 metadata.set('COLORTERM2', 0.0) 

708 metadata.set('COLORTERM3', 0.0) 

709 except Exception as e: 

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

711 

712 def copyIcSourceFields(self, icSourceCat, sourceCat): 

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

714 

715 The fields copied are those specified by 

716 ``config.icSourceFieldsToCopy``. 

717 

718 Parameters 

719 ---------- 

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

721 Catalog from which to copy fields. 

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

723 Catalog to which to copy fields. 

724 

725 Raises 

726 ------ 

727 RuntimeError 

728 Raised if any of the following occur: 

729 - icSourceSchema and icSourceKeys are not specified. 

730 - icSourceCat and sourceCat are not specified. 

731 - icSourceFieldsToCopy is empty. 

732 """ 

733 if self.schemaMapper is None: 

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

735 "icSourceSchema and icSourceKeys when " 

736 "constructing this task") 

737 if icSourceCat is None or sourceCat is None: 

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

739 "specified") 

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

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

742 "icSourceFieldsToCopy is empty") 

743 return 

744 

745 mc = afwTable.MatchControl() 

746 mc.findOnlyClosest = False # return all matched objects 

747 matches = afwTable.matchXy(icSourceCat, sourceCat, 

748 self.config.matchRadiusPix, mc) 

749 if self.config.doDeblend: 

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

751 # if deblended, keep children 

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

753 

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

755 # need to prune to the best matches 

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

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

758 bestMatches = {} 

759 for m0, m1, d in matches: 

760 id0 = m0.getId() 

761 match = bestMatches.get(id0) 

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

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

764 matches = list(bestMatches.values()) 

765 

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

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

768 # that ID as the key in bestMatches) 

769 numMatches = len(matches) 

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

771 if numUniqueSources != numMatches: 

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

773 "sources", numMatches, numUniqueSources) 

774 

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

776 "%d sources", numMatches) 

777 

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

779 # fields 

780 for icSrc, src, d in matches: 

781 src.setFlag(self.calibSourceKey, True) 

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

783 # (DM-407) 

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

785 # then restore it 

786 icSrcFootprint = icSrc.getFootprint() 

787 try: 

788 icSrc.setFootprint(src.getFootprint()) 

789 src.assign(icSrc, self.schemaMapper) 

790 finally: 

791 icSrc.setFootprint(icSrcFootprint)