Coverage for python/lsst/meas/base/forcedPhotCcd.py: 23%

275 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-19 02:08 -0800

1# This file is part of meas_base. 

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 warnings 

23 

24import pandas as pd 

25import numpy as np 

26 

27import lsst.pex.config 

28import lsst.pex.exceptions 

29import lsst.pipe.base 

30import lsst.geom 

31import lsst.afw.detection 

32import lsst.afw.geom 

33import lsst.afw.image 

34import lsst.afw.table 

35import lsst.sphgeom 

36 

37from lsst.obs.base import ExposureIdInfo 

38from lsst.pipe.base import PipelineTaskConnections 

39import lsst.pipe.base.connectionTypes as cT 

40 

41import lsst.pipe.base as pipeBase 

42from lsst.skymap import BaseSkyMap 

43 

44from .forcedMeasurement import ForcedMeasurementTask 

45from .applyApCorr import ApplyApCorrTask 

46from .catalogCalculation import CatalogCalculationTask 

47 

48__all__ = ("ForcedPhotCcdConfig", "ForcedPhotCcdTask", 

49 "ForcedPhotCcdFromDataFrameTask", "ForcedPhotCcdFromDataFrameConfig") 

50 

51 

52class ForcedPhotCcdConnections(PipelineTaskConnections, 

53 dimensions=("instrument", "visit", "detector", "skymap", "tract"), 

54 defaultTemplates={"inputCoaddName": "deep", 

55 "inputName": "calexp", 

56 "skyWcsName": "jointcal", 

57 "photoCalibName": "fgcm"}): 

58 inputSchema = cT.InitInput( 

59 doc="Schema for the input measurement catalogs.", 

60 name="{inputCoaddName}Coadd_ref_schema", 

61 storageClass="SourceCatalog", 

62 ) 

63 outputSchema = cT.InitOutput( 

64 doc="Schema for the output forced measurement catalogs.", 

65 name="forced_src_schema", 

66 storageClass="SourceCatalog", 

67 ) 

68 exposure = cT.Input( 

69 doc="Input exposure to perform photometry on.", 

70 name="{inputName}", 

71 storageClass="ExposureF", 

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

73 ) 

74 refCat = cT.Input( 

75 doc="Catalog of shapes and positions at which to force photometry.", 

76 name="{inputCoaddName}Coadd_ref", 

77 storageClass="SourceCatalog", 

78 dimensions=["skymap", "tract", "patch"], 

79 multiple=True, 

80 deferLoad=True, 

81 ) 

82 skyMap = cT.Input( 

83 doc="SkyMap dataset that defines the coordinate system of the reference catalog.", 

84 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME, 

85 storageClass="SkyMap", 

86 dimensions=["skymap"], 

87 ) 

88 skyCorr = cT.Input( 

89 doc="Input Sky Correction to be subtracted from the calexp if doApplySkyCorr=True", 

90 name="skyCorr", 

91 storageClass="Background", 

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

93 ) 

94 externalSkyWcsTractCatalog = cT.Input( 

95 doc=("Per-tract, per-visit wcs calibrations. These catalogs use the detector " 

96 "id for the catalog id, sorted on id for fast lookup."), 

97 name="{skyWcsName}SkyWcsCatalog", 

98 storageClass="ExposureCatalog", 

99 dimensions=["instrument", "visit", "tract"], 

100 ) 

101 externalSkyWcsGlobalCatalog = cT.Input( 

102 doc=("Per-visit wcs calibrations computed globally (with no tract information). " 

103 "These catalogs use the detector id for the catalog id, sorted on id for " 

104 "fast lookup."), 

105 name="{skyWcsName}SkyWcsCatalog", 

106 storageClass="ExposureCatalog", 

107 dimensions=["instrument", "visit"], 

108 ) 

109 externalPhotoCalibTractCatalog = cT.Input( 

110 doc=("Per-tract, per-visit photometric calibrations. These catalogs use the " 

111 "detector id for the catalog id, sorted on id for fast lookup."), 

112 name="{photoCalibName}PhotoCalibCatalog", 

113 storageClass="ExposureCatalog", 

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

115 ) 

116 externalPhotoCalibGlobalCatalog = cT.Input( 

117 doc=("Per-visit photometric calibrations computed globally (with no tract " 

118 "information). These catalogs use the detector id for the catalog id, " 

119 "sorted on id for fast lookup."), 

120 name="{photoCalibName}PhotoCalibCatalog", 

121 storageClass="ExposureCatalog", 

122 dimensions=["instrument", "visit"], 

123 ) 

124 finalizedPsfApCorrCatalog = cT.Input( 

125 doc=("Per-visit finalized psf models and aperture correction maps. " 

126 "These catalogs use the detector id for the catalog id, " 

127 "sorted on id for fast lookup."), 

128 name="finalized_psf_ap_corr_catalog", 

129 storageClass="ExposureCatalog", 

130 dimensions=["instrument", "visit"], 

131 ) 

132 measCat = cT.Output( 

133 doc="Output forced photometry catalog.", 

134 name="forced_src", 

135 storageClass="SourceCatalog", 

136 dimensions=["instrument", "visit", "detector", "skymap", "tract"], 

137 ) 

138 

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

140 super().__init__(config=config) 

141 if not config.doApplySkyCorr: 

142 self.inputs.remove("skyCorr") 

143 if config.doApplyExternalSkyWcs: 

144 if config.useGlobalExternalSkyWcs: 

145 self.inputs.remove("externalSkyWcsTractCatalog") 

146 else: 

147 self.inputs.remove("externalSkyWcsGlobalCatalog") 

148 else: 

149 self.inputs.remove("externalSkyWcsTractCatalog") 

150 self.inputs.remove("externalSkyWcsGlobalCatalog") 

151 if config.doApplyExternalPhotoCalib: 

152 if config.useGlobalExternalPhotoCalib: 

153 self.inputs.remove("externalPhotoCalibTractCatalog") 

154 else: 

155 self.inputs.remove("externalPhotoCalibGlobalCatalog") 

156 else: 

157 self.inputs.remove("externalPhotoCalibTractCatalog") 

158 self.inputs.remove("externalPhotoCalibGlobalCatalog") 

159 if not config.doApplyFinalizedPsf: 

160 self.inputs.remove("finalizedPsfApCorrCatalog") 

161 

162 

163class ForcedPhotCcdConfig(pipeBase.PipelineTaskConfig, 

164 pipelineConnections=ForcedPhotCcdConnections): 

165 """Config class for forced measurement driver task.""" 

166 measurement = lsst.pex.config.ConfigurableField( 

167 target=ForcedMeasurementTask, 

168 doc="subtask to do forced measurement" 

169 ) 

170 coaddName = lsst.pex.config.Field( 

171 doc="coadd name: typically one of deep or goodSeeing", 

172 dtype=str, 

173 default="deep", 

174 ) 

175 doApCorr = lsst.pex.config.Field( 

176 dtype=bool, 

177 default=True, 

178 doc="Run subtask to apply aperture corrections" 

179 ) 

180 applyApCorr = lsst.pex.config.ConfigurableField( 

181 target=ApplyApCorrTask, 

182 doc="Subtask to apply aperture corrections" 

183 ) 

184 catalogCalculation = lsst.pex.config.ConfigurableField( 

185 target=CatalogCalculationTask, 

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

187 ) 

188 doApplyUberCal = lsst.pex.config.Field( 

189 dtype=bool, 

190 doc="Apply meas_mosaic ubercal results to input calexps?", 

191 default=False, 

192 deprecated="Deprecated by DM-23352; use doApplyExternalPhotoCalib and doApplyExternalSkyWcs instead", 

193 ) 

194 doApplyExternalPhotoCalib = lsst.pex.config.Field( 

195 dtype=bool, 

196 default=False, 

197 doc=("Whether to apply external photometric calibration via an " 

198 "`lsst.afw.image.PhotoCalib` object."), 

199 ) 

200 useGlobalExternalPhotoCalib = lsst.pex.config.Field( 

201 dtype=bool, 

202 default=True, 

203 doc=("When using doApplyExternalPhotoCalib, use 'global' calibrations " 

204 "that are not run per-tract. When False, use per-tract photometric " 

205 "calibration files.") 

206 ) 

207 doApplyExternalSkyWcs = lsst.pex.config.Field( 

208 dtype=bool, 

209 default=False, 

210 doc=("Whether to apply external astrometric calibration via an " 

211 "`lsst.afw.geom.SkyWcs` object."), 

212 ) 

213 useGlobalExternalSkyWcs = lsst.pex.config.Field( 

214 dtype=bool, 

215 default=False, 

216 doc=("When using doApplyExternalSkyWcs, use 'global' calibrations " 

217 "that are not run per-tract. When False, use per-tract wcs " 

218 "files.") 

219 ) 

220 doApplyFinalizedPsf = lsst.pex.config.Field( 

221 dtype=bool, 

222 default=False, 

223 doc="Whether to apply finalized psf models and aperture correction map.", 

224 ) 

225 doApplySkyCorr = lsst.pex.config.Field( 

226 dtype=bool, 

227 default=False, 

228 doc="Apply sky correction?", 

229 ) 

230 includePhotoCalibVar = lsst.pex.config.Field( 

231 dtype=bool, 

232 default=False, 

233 doc="Add photometric calibration variance to warp variance plane?", 

234 ) 

235 footprintSource = lsst.pex.config.ChoiceField( 

236 dtype=str, 

237 doc="Where to obtain footprints to install in the measurement catalog, prior to measurement.", 

238 allowed={ 

239 "transformed": "Transform footprints from the reference catalog (downgrades HeavyFootprints).", 

240 "psf": ("Use the scaled shape of the PSF at the position of each source (does not generate " 

241 "HeavyFootprints)."), 

242 }, 

243 optional=True, 

244 default="transformed", 

245 ) 

246 psfFootprintScaling = lsst.pex.config.Field( 

247 dtype=float, 

248 doc="Scaling factor to apply to the PSF shape when footprintSource='psf' (ignored otherwise).", 

249 default=3.0, 

250 ) 

251 

252 def setDefaults(self): 

253 # Docstring inherited. 

254 # Make catalogCalculation a no-op by default as no modelFlux is setup 

255 # by default in ForcedMeasurementTask. 

256 super().setDefaults() 

257 self.measurement.plugins.names |= ['base_LocalPhotoCalib', 'base_LocalWcs'] 

258 self.catalogCalculation.plugins.names = [] 

259 

260 

261class ForcedPhotCcdTask(pipeBase.PipelineTask): 

262 """A pipeline task for performing forced measurement on CCD images. 

263 

264 Parameters 

265 ---------- 

266 butler : `None` 

267 Deprecated and unused. Should always be `None`. 

268 refSchema : `lsst.afw.table.Schema`, optional 

269 The schema of the reference catalog, passed to the constructor of the 

270 references subtask. Optional, but must be specified if ``initInputs`` 

271 is not; if both are specified, ``initInputs`` takes precedence. 

272 initInputs : `dict` 

273 Dictionary that can contain a key ``inputSchema`` containing the 

274 schema. If present will override the value of ``refSchema``. 

275 **kwds 

276 Keyword arguments are passed to the supertask constructor. 

277 """ 

278 

279 ConfigClass = ForcedPhotCcdConfig 

280 _DefaultName = "forcedPhotCcd" 

281 dataPrefix = "" 

282 

283 def __init__(self, butler=None, refSchema=None, initInputs=None, **kwds): 

284 super().__init__(**kwds) 

285 

286 if butler is not None: 

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

288 category=FutureWarning, stacklevel=2) 

289 butler = None 

290 

291 if initInputs is not None: 

292 refSchema = initInputs['inputSchema'].schema 

293 

294 if refSchema is None: 

295 raise ValueError("No reference schema provided.") 

296 

297 self.makeSubtask("measurement", refSchema=refSchema) 

298 # It is necessary to get the schema internal to the forced measurement 

299 # task until such a time that the schema is not owned by the 

300 # measurement task, but is passed in by an external caller. 

301 if self.config.doApCorr: 

302 self.makeSubtask("applyApCorr", schema=self.measurement.schema) 

303 self.makeSubtask('catalogCalculation', schema=self.measurement.schema) 

304 self.outputSchema = lsst.afw.table.SourceCatalog(self.measurement.schema) 

305 

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

307 inputs = butlerQC.get(inputRefs) 

308 

309 tract = butlerQC.quantum.dataId['tract'] 

310 skyMap = inputs.pop('skyMap') 

311 inputs['refWcs'] = skyMap[tract].getWcs() 

312 

313 # Connections only exist if they are configured to be used. 

314 skyCorr = inputs.pop('skyCorr', None) 

315 if self.config.useGlobalExternalSkyWcs: 

316 externalSkyWcsCatalog = inputs.pop('externalSkyWcsGlobalCatalog', None) 

317 else: 

318 externalSkyWcsCatalog = inputs.pop('externalSkyWcsTractCatalog', None) 

319 if self.config.useGlobalExternalPhotoCalib: 

320 externalPhotoCalibCatalog = inputs.pop('externalPhotoCalibGlobalCatalog', None) 

321 else: 

322 externalPhotoCalibCatalog = inputs.pop('externalPhotoCalibTractCatalog', None) 

323 finalizedPsfApCorrCatalog = inputs.pop('finalizedPsfApCorrCatalog', None) 

324 

325 inputs['exposure'] = self.prepareCalibratedExposure( 

326 inputs['exposure'], 

327 skyCorr=skyCorr, 

328 externalSkyWcsCatalog=externalSkyWcsCatalog, 

329 externalPhotoCalibCatalog=externalPhotoCalibCatalog, 

330 finalizedPsfApCorrCatalog=finalizedPsfApCorrCatalog 

331 ) 

332 

333 inputs['refCat'] = self.mergeAndFilterReferences(inputs['exposure'], inputs['refCat'], 

334 inputs['refWcs']) 

335 

336 if inputs['refCat'] is None: 

337 self.log.info("No WCS for exposure %s. No %s catalog will be written.", 

338 butlerQC.quantum.dataId, outputRefs.measCat.datasetType.name) 

339 else: 

340 inputs['measCat'], inputs['exposureId'] = self.generateMeasCat(inputRefs.exposure.dataId, 

341 inputs['exposure'], 

342 inputs['refCat'], inputs['refWcs'], 

343 "visit_detector") 

344 self.attachFootprints(inputs['measCat'], inputs['refCat'], inputs['exposure'], inputs['refWcs']) 

345 outputs = self.run(**inputs) 

346 butlerQC.put(outputs, outputRefs) 

347 

348 def prepareCalibratedExposure(self, exposure, skyCorr=None, externalSkyWcsCatalog=None, 

349 externalPhotoCalibCatalog=None, finalizedPsfApCorrCatalog=None): 

350 """Prepare a calibrated exposure and apply external calibrations 

351 and sky corrections if so configured. 

352 

353 Parameters 

354 ---------- 

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

356 Input exposure to adjust calibrations. 

357 skyCorr : `lsst.afw.math.backgroundList`, optional 

358 Sky correction frame to apply if doApplySkyCorr=True. 

359 externalSkyWcsCatalog : `lsst.afw.table.ExposureCatalog`, optional 

360 Exposure catalog with external skyWcs to be applied 

361 if config.doApplyExternalSkyWcs=True. Catalog uses the detector id 

362 for the catalog id, sorted on id for fast lookup. 

363 externalPhotoCalibCatalog : `lsst.afw.table.ExposureCatalog`, optional 

364 Exposure catalog with external photoCalib to be applied 

365 if config.doApplyExternalPhotoCalib=True. Catalog uses the detector 

366 id for the catalog id, sorted on id for fast lookup. 

367 finalizedPsfApCorrCatalog : `lsst.afw.table.ExposureCatalog`, optional 

368 Exposure catalog with finalized psf models and aperture correction 

369 maps to be applied if config.doApplyFinalizedPsf=True. Catalog uses 

370 the detector id for the catalog id, sorted on id for fast lookup. 

371 

372 Returns 

373 ------- 

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

375 Exposure with adjusted calibrations. 

376 """ 

377 detectorId = exposure.getInfo().getDetector().getId() 

378 

379 if externalPhotoCalibCatalog is not None: 

380 row = externalPhotoCalibCatalog.find(detectorId) 

381 if row is None: 

382 self.log.warning("Detector id %s not found in externalPhotoCalibCatalog; " 

383 "Using original photoCalib.", detectorId) 

384 else: 

385 photoCalib = row.getPhotoCalib() 

386 if photoCalib is None: 

387 self.log.warning("Detector id %s has None for photoCalib in externalPhotoCalibCatalog; " 

388 "Using original photoCalib.", detectorId) 

389 else: 

390 exposure.setPhotoCalib(photoCalib) 

391 

392 if externalSkyWcsCatalog is not None: 

393 row = externalSkyWcsCatalog.find(detectorId) 

394 if row is None: 

395 self.log.warning("Detector id %s not found in externalSkyWcsCatalog; " 

396 "Using original skyWcs.", detectorId) 

397 else: 

398 skyWcs = row.getWcs() 

399 if skyWcs is None: 

400 self.log.warning("Detector id %s has None for skyWcs in externalSkyWcsCatalog; " 

401 "Using original skyWcs.", detectorId) 

402 else: 

403 exposure.setWcs(skyWcs) 

404 

405 if finalizedPsfApCorrCatalog is not None: 

406 row = finalizedPsfApCorrCatalog.find(detectorId) 

407 if row is None: 

408 self.log.warning("Detector id %s not found in finalizedPsfApCorrCatalog; " 

409 "Using original psf.", detectorId) 

410 else: 

411 psf = row.getPsf() 

412 apCorrMap = row.getApCorrMap() 

413 if psf is None or apCorrMap is None: 

414 self.log.warning("Detector id %s has None for psf/apCorrMap in " 

415 "finalizedPsfApCorrCatalog; Using original psf.", detectorId) 

416 else: 

417 exposure.setPsf(psf) 

418 exposure.setApCorrMap(apCorrMap) 

419 

420 if skyCorr is not None: 

421 exposure.maskedImage -= skyCorr.getImage() 

422 

423 return exposure 

424 

425 def mergeAndFilterReferences(self, exposure, refCats, refWcs): 

426 """Filter reference catalog so that all sources are within the 

427 boundaries of the exposure. 

428 

429 Parameters 

430 ---------- 

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

432 Exposure to generate the catalog for. 

433 refCats : sequence of `lsst.daf.butler.DeferredDatasetHandle` 

434 Handles for catalogs of shapes and positions at which to force 

435 photometry. 

436 refWcs : `lsst.afw.image.SkyWcs` 

437 Reference world coordinate system. 

438 

439 Returns 

440 ------- 

441 refSources : `lsst.afw.table.SourceCatalog` 

442 Filtered catalog of forced sources to measure. 

443 

444 Notes 

445 ----- 

446 The majority of this code is based on the methods of 

447 lsst.meas.algorithms.loadReferenceObjects.ReferenceObjectLoader 

448 

449 """ 

450 mergedRefCat = None 

451 

452 # Step 1: Determine bounds of the exposure photometry will 

453 # be performed on. 

454 expWcs = exposure.getWcs() 

455 if expWcs is None: 

456 self.log.info("Exposure has no WCS. Returning None for mergedRefCat.") 

457 else: 

458 expRegion = exposure.getBBox(lsst.afw.image.PARENT) 

459 expBBox = lsst.geom.Box2D(expRegion) 

460 expBoxCorners = expBBox.getCorners() 

461 expSkyCorners = [expWcs.pixelToSky(corner).getVector() for 

462 corner in expBoxCorners] 

463 expPolygon = lsst.sphgeom.ConvexPolygon(expSkyCorners) 

464 

465 # Step 2: Filter out reference catalog sources that are 

466 # not contained within the exposure boundaries, or whose 

467 # parents are not within the exposure boundaries. Note 

468 # that within a single input refCat, the parents always 

469 # appear before the children. 

470 for refCat in refCats: 

471 refCat = refCat.get() 

472 if mergedRefCat is None: 

473 mergedRefCat = lsst.afw.table.SourceCatalog(refCat.table) 

474 containedIds = {0} # zero as a parent ID means "this is a parent" 

475 for record in refCat: 

476 if (expPolygon.contains(record.getCoord().getVector()) and record.getParent() 

477 in containedIds): 

478 record.setFootprint(record.getFootprint()) 

479 mergedRefCat.append(record) 

480 containedIds.add(record.getId()) 

481 if mergedRefCat is None: 

482 raise RuntimeError("No reference objects for forced photometry.") 

483 mergedRefCat.sort(lsst.afw.table.SourceTable.getParentKey()) 

484 return mergedRefCat 

485 

486 def generateMeasCat(self, exposureDataId, exposure, refCat, refWcs, idPackerName): 

487 """Generate a measurement catalog. 

488 

489 Parameters 

490 ---------- 

491 exposureDataId : `DataId` 

492 Butler dataId for this exposure. 

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

494 Exposure to generate the catalog for. 

495 refCat : `lsst.afw.table.SourceCatalog` 

496 Catalog of shapes and positions at which to force photometry. 

497 refWcs : `lsst.afw.image.SkyWcs` 

498 Reference world coordinate system. 

499 This parameter is not currently used. 

500 idPackerName : `str` 

501 Type of ID packer to construct from the registry. 

502 

503 Returns 

504 ------- 

505 measCat : `lsst.afw.table.SourceCatalog` 

506 Catalog of forced sources to measure. 

507 expId : `int` 

508 Unique binary id associated with the input exposure 

509 """ 

510 exposureIdInfo = ExposureIdInfo.fromDataId(exposureDataId, idPackerName) 

511 idFactory = exposureIdInfo.makeSourceIdFactory() 

512 

513 measCat = self.measurement.generateMeasCat(exposure, refCat, refWcs, 

514 idFactory=idFactory) 

515 return measCat, exposureIdInfo.expId 

516 

517 def run(self, measCat, exposure, refCat, refWcs, exposureId=None): 

518 """Perform forced measurement on a single exposure. 

519 

520 Parameters 

521 ---------- 

522 measCat : `lsst.afw.table.SourceCatalog` 

523 The measurement catalog, based on the sources listed in the 

524 reference catalog. 

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

526 The measurement image upon which to perform forced detection. 

527 refCat : `lsst.afw.table.SourceCatalog` 

528 The reference catalog of sources to measure. 

529 refWcs : `lsst.afw.image.SkyWcs` 

530 The WCS for the references. 

531 exposureId : `int` 

532 Optional unique exposureId used for random seed in measurement 

533 task. 

534 

535 Returns 

536 ------- 

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

538 Structure with fields: 

539 

540 ``measCat`` 

541 Catalog of forced measurement results 

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

543 """ 

544 self.measurement.run(measCat, exposure, refCat, refWcs, exposureId=exposureId) 

545 if self.config.doApCorr: 

546 self.applyApCorr.run( 

547 catalog=measCat, 

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

549 ) 

550 self.catalogCalculation.run(measCat) 

551 

552 return pipeBase.Struct(measCat=measCat) 

553 

554 def attachFootprints(self, sources, refCat, exposure, refWcs): 

555 """Attach footprints to blank sources prior to measurements. 

556 

557 Notes 

558 ----- 

559 `~lsst.afw.detection.Footprint` objects for forced photometry must 

560 be in the pixel coordinate system of the image being measured, while 

561 the actual detections may start out in a different coordinate system. 

562 

563 Subclasses of this class may implement this method to define how 

564 those `~lsst.afw.detection.Footprint` objects should be generated. 

565 

566 This default implementation transforms depends on the 

567 ``footprintSource`` configuration parameter. 

568 """ 

569 if self.config.footprintSource == "transformed": 

570 return self.measurement.attachTransformedFootprints(sources, refCat, exposure, refWcs) 

571 elif self.config.footprintSource == "psf": 

572 return self.measurement.attachPsfShapeFootprints(sources, exposure, 

573 scaling=self.config.psfFootprintScaling) 

574 

575 

576class ForcedPhotCcdFromDataFrameConnections(PipelineTaskConnections, 

577 dimensions=("instrument", "visit", "detector", "skymap", "tract"), 

578 defaultTemplates={"inputCoaddName": "goodSeeing", 

579 "inputName": "calexp", 

580 "skyWcsName": "jointcal", 

581 "photoCalibName": "fgcm"}): 

582 refCat = cT.Input( 

583 doc="Catalog of positions at which to force photometry.", 

584 name="{inputCoaddName}Diff_fullDiaObjTable", 

585 storageClass="DataFrame", 

586 dimensions=["skymap", "tract", "patch"], 

587 multiple=True, 

588 deferLoad=True, 

589 ) 

590 exposure = cT.Input( 

591 doc="Input exposure to perform photometry on.", 

592 name="{inputName}", 

593 storageClass="ExposureF", 

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

595 ) 

596 skyCorr = cT.Input( 

597 doc="Input Sky Correction to be subtracted from the calexp if doApplySkyCorr=True", 

598 name="skyCorr", 

599 storageClass="Background", 

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

601 ) 

602 externalSkyWcsTractCatalog = cT.Input( 

603 doc=("Per-tract, per-visit wcs calibrations. These catalogs use the detector " 

604 "id for the catalog id, sorted on id for fast lookup."), 

605 name="{skyWcsName}SkyWcsCatalog", 

606 storageClass="ExposureCatalog", 

607 dimensions=["instrument", "visit", "tract"], 

608 ) 

609 externalSkyWcsGlobalCatalog = cT.Input( 

610 doc=("Per-visit wcs calibrations computed globally (with no tract information). " 

611 "These catalogs use the detector id for the catalog id, sorted on id for " 

612 "fast lookup."), 

613 name="{skyWcsName}SkyWcsCatalog", 

614 storageClass="ExposureCatalog", 

615 dimensions=["instrument", "visit"], 

616 ) 

617 externalPhotoCalibTractCatalog = cT.Input( 

618 doc=("Per-tract, per-visit photometric calibrations. These catalogs use the " 

619 "detector id for the catalog id, sorted on id for fast lookup."), 

620 name="{photoCalibName}PhotoCalibCatalog", 

621 storageClass="ExposureCatalog", 

622 dimensions=["instrument", "visit", "tract"], 

623 ) 

624 externalPhotoCalibGlobalCatalog = cT.Input( 

625 doc=("Per-visit photometric calibrations computed globally (with no tract " 

626 "information). These catalogs use the detector id for the catalog id, " 

627 "sorted on id for fast lookup."), 

628 name="{photoCalibName}PhotoCalibCatalog", 

629 storageClass="ExposureCatalog", 

630 dimensions=["instrument", "visit"], 

631 ) 

632 finalizedPsfApCorrCatalog = cT.Input( 

633 doc=("Per-visit finalized psf models and aperture correction maps. " 

634 "These catalogs use the detector id for the catalog id, " 

635 "sorted on id for fast lookup."), 

636 name="finalized_psf_ap_corr_catalog", 

637 storageClass="ExposureCatalog", 

638 dimensions=["instrument", "visit"], 

639 ) 

640 measCat = cT.Output( 

641 doc="Output forced photometry catalog.", 

642 name="forced_src_diaObject", 

643 storageClass="SourceCatalog", 

644 dimensions=["instrument", "visit", "detector", "skymap", "tract"], 

645 ) 

646 outputSchema = cT.InitOutput( 

647 doc="Schema for the output forced measurement catalogs.", 

648 name="forced_src_diaObject_schema", 

649 storageClass="SourceCatalog", 

650 ) 

651 

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

653 super().__init__(config=config) 

654 if not config.doApplySkyCorr: 

655 self.inputs.remove("skyCorr") 

656 if config.doApplyExternalSkyWcs: 

657 if config.useGlobalExternalSkyWcs: 

658 self.inputs.remove("externalSkyWcsTractCatalog") 

659 else: 

660 self.inputs.remove("externalSkyWcsGlobalCatalog") 

661 else: 

662 self.inputs.remove("externalSkyWcsTractCatalog") 

663 self.inputs.remove("externalSkyWcsGlobalCatalog") 

664 if config.doApplyExternalPhotoCalib: 

665 if config.useGlobalExternalPhotoCalib: 

666 self.inputs.remove("externalPhotoCalibTractCatalog") 

667 else: 

668 self.inputs.remove("externalPhotoCalibGlobalCatalog") 

669 else: 

670 self.inputs.remove("externalPhotoCalibTractCatalog") 

671 self.inputs.remove("externalPhotoCalibGlobalCatalog") 

672 if not config.doApplyFinalizedPsf: 

673 self.inputs.remove("finalizedPsfApCorrCatalog") 

674 

675 

676class ForcedPhotCcdFromDataFrameConfig(ForcedPhotCcdConfig, 

677 pipelineConnections=ForcedPhotCcdFromDataFrameConnections): 

678 def setDefaults(self): 

679 super().setDefaults() 

680 self.footprintSource = "psf" 

681 self.measurement.doReplaceWithNoise = False 

682 self.measurement.plugins.names = ["base_LocalPhotoCalib", "base_LocalWcs", "base_LocalBackground", 

683 "base_TransformedCentroidFromCoord", "base_PsfFlux", 

684 "base_PixelFlags"] 

685 self.measurement.copyColumns = {'id': 'diaObjectId', 'coord_ra': 'coord_ra', 'coord_dec': 'coord_dec'} 

686 self.measurement.slots.centroid = "base_TransformedCentroidFromCoord" 

687 self.measurement.slots.psfFlux = "base_PsfFlux" 

688 self.measurement.slots.shape = None 

689 

690 def validate(self): 

691 super().validate() 

692 if self.footprintSource == "transformed": 

693 raise ValueError("Cannot transform footprints from reference catalog, " 

694 "because DataFrames can't hold footprints.") 

695 

696 

697class ForcedPhotCcdFromDataFrameTask(ForcedPhotCcdTask): 

698 """Force Photometry on a per-detector exposure with coords from a DataFrame 

699 

700 Uses input from a DataFrame instead of SourceCatalog 

701 like the base class ForcedPhotCcd does. 

702 Writes out a SourceCatalog so that the downstream 

703 WriteForcedSourceTableTask can be reused with output from this Task. 

704 """ 

705 _DefaultName = "forcedPhotCcdFromDataFrame" 

706 ConfigClass = ForcedPhotCcdFromDataFrameConfig 

707 

708 def __init__(self, butler=None, refSchema=None, initInputs=None, **kwds): 

709 # Parent's init assumes that we have a reference schema; Cannot reuse 

710 pipeBase.PipelineTask.__init__(self, **kwds) 

711 

712 if butler is not None: 

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

714 category=FutureWarning, stacklevel=2) 

715 butler = None 

716 

717 self.makeSubtask("measurement", refSchema=lsst.afw.table.SourceTable.makeMinimalSchema()) 

718 

719 if self.config.doApCorr: 

720 self.makeSubtask("applyApCorr", schema=self.measurement.schema) 

721 self.makeSubtask('catalogCalculation', schema=self.measurement.schema) 

722 self.outputSchema = lsst.afw.table.SourceCatalog(self.measurement.schema) 

723 

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

725 inputs = butlerQC.get(inputRefs) 

726 

727 # When run with dataframes, we do not need a reference wcs. 

728 inputs['refWcs'] = None 

729 

730 # Connections only exist if they are configured to be used. 

731 skyCorr = inputs.pop('skyCorr', None) 

732 if self.config.useGlobalExternalSkyWcs: 

733 externalSkyWcsCatalog = inputs.pop('externalSkyWcsGlobalCatalog', None) 

734 else: 

735 externalSkyWcsCatalog = inputs.pop('externalSkyWcsTractCatalog', None) 

736 if self.config.useGlobalExternalPhotoCalib: 

737 externalPhotoCalibCatalog = inputs.pop('externalPhotoCalibGlobalCatalog', None) 

738 else: 

739 externalPhotoCalibCatalog = inputs.pop('externalPhotoCalibTractCatalog', None) 

740 finalizedPsfApCorrCatalog = inputs.pop('finalizedPsfApCorrCatalog', None) 

741 

742 inputs['exposure'] = self.prepareCalibratedExposure( 

743 inputs['exposure'], 

744 skyCorr=skyCorr, 

745 externalSkyWcsCatalog=externalSkyWcsCatalog, 

746 externalPhotoCalibCatalog=externalPhotoCalibCatalog, 

747 finalizedPsfApCorrCatalog=finalizedPsfApCorrCatalog 

748 ) 

749 

750 self.log.info("Filtering ref cats: %s", ','.join([str(i.dataId) for i in inputs['refCat']])) 

751 if inputs["exposure"].getWcs() is not None: 

752 refCat = self.df2RefCat([i.get(parameters={"columns": ['diaObjectId', 'ra', 'decl']}) 

753 for i in inputs['refCat']], 

754 inputs['exposure'].getBBox(), inputs['exposure'].getWcs()) 

755 inputs['refCat'] = refCat 

756 # generateMeasCat does not use the refWcs. 

757 inputs['measCat'], inputs['exposureId'] = self.generateMeasCat( 

758 inputRefs.exposure.dataId, inputs['exposure'], inputs['refCat'], inputs['refWcs'], 

759 "visit_detector") 

760 # attachFootprints only uses refWcs in ``transformed`` mode, which is not 

761 # supported in the DataFrame-backed task. 

762 self.attachFootprints(inputs["measCat"], inputs["refCat"], inputs["exposure"], inputs["refWcs"]) 

763 outputs = self.run(**inputs) 

764 

765 butlerQC.put(outputs, outputRefs) 

766 else: 

767 self.log.info("No WCS for %s. Skipping and no %s catalog will be written.", 

768 butlerQC.quantum.dataId, outputRefs.measCat.datasetType.name) 

769 

770 def df2RefCat(self, dfList, exposureBBox, exposureWcs): 

771 """Convert list of DataFrames to reference catalog 

772 

773 Concatenate list of DataFrames presumably from multiple patches and 

774 downselect rows that overlap the exposureBBox using the exposureWcs. 

775 

776 Parameters 

777 ---------- 

778 dfList : `list` of `pandas.DataFrame` 

779 Each element containst diaObjects with ra/decl position in degrees 

780 Columns 'diaObjectId', 'ra', 'decl' are expected 

781 exposureBBox : `lsst.geom.Box2I` 

782 Bounding box on which to select rows that overlap 

783 exposureWcs : `lsst.afw.geom.SkyWcs` 

784 World coordinate system to convert sky coords in ref cat to 

785 pixel coords with which to compare with exposureBBox 

786 

787 Returns 

788 ------- 

789 refCat : `lsst.afw.table.SourceTable` 

790 Source Catalog with minimal schema that overlaps exposureBBox 

791 """ 

792 df = pd.concat(dfList) 

793 # translate ra/decl coords in dataframe to detector pixel coords 

794 # to down select rows that overlap the detector bbox 

795 mapping = exposureWcs.getTransform().getMapping() 

796 x, y = mapping.applyInverse(np.array(df[['ra', 'decl']].values*2*np.pi/360).T) 

797 inBBox = lsst.geom.Box2D(exposureBBox).contains(x, y) 

798 refCat = self.df2SourceCat(df[inBBox]) 

799 return refCat 

800 

801 def df2SourceCat(self, df): 

802 """Create minimal schema SourceCatalog from a pandas DataFrame. 

803 

804 The forced measurement subtask expects this as input. 

805 

806 Parameters 

807 ---------- 

808 df : `pandas.DataFrame` 

809 DiaObjects with locations and ids. 

810 

811 Returns 

812 ------- 

813 outputCatalog : `lsst.afw.table.SourceTable` 

814 Output catalog with minimal schema. 

815 """ 

816 schema = lsst.afw.table.SourceTable.makeMinimalSchema() 

817 outputCatalog = lsst.afw.table.SourceCatalog(schema) 

818 outputCatalog.reserve(len(df)) 

819 

820 for diaObjectId, ra, decl in df[['ra', 'decl']].itertuples(): 

821 outputRecord = outputCatalog.addNew() 

822 outputRecord.setId(diaObjectId) 

823 outputRecord.setCoord(lsst.geom.SpherePoint(ra, decl, lsst.geom.degrees)) 

824 return outputCatalog