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

280 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-10-11 02:03 -0700

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 def getSchemaCatalogs(self): 

576 """The schema catalogs that will be used by this task. 

577 

578 Returns 

579 ------- 

580 schemaCatalogs : `dict` 

581 Dictionary mapping dataset type to schema catalog. 

582 

583 Notes 

584 ----- 

585 There is only one schema for each type of forced measurement. The 

586 dataset type for this measurement is defined in the mapper. 

587 """ 

588 catalog = lsst.afw.table.SourceCatalog(self.measurement.schema) 

589 catalog.getTable().setMetadata(self.measurement.algMetadata) 

590 datasetType = self.dataPrefix + "forced_src" 

591 return {datasetType: catalog} 

592 

593 

594class ForcedPhotCcdFromDataFrameConnections(PipelineTaskConnections, 

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

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

597 "inputName": "calexp", 

598 "skyWcsName": "jointcal", 

599 "photoCalibName": "fgcm"}): 

600 refCat = cT.Input( 

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

602 name="{inputCoaddName}Diff_fullDiaObjTable", 

603 storageClass="DataFrame", 

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

605 multiple=True, 

606 deferLoad=True, 

607 ) 

608 exposure = cT.Input( 

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

610 name="{inputName}", 

611 storageClass="ExposureF", 

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

613 ) 

614 skyCorr = cT.Input( 

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

616 name="skyCorr", 

617 storageClass="Background", 

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

619 ) 

620 externalSkyWcsTractCatalog = cT.Input( 

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

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

623 name="{skyWcsName}SkyWcsCatalog", 

624 storageClass="ExposureCatalog", 

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

626 ) 

627 externalSkyWcsGlobalCatalog = cT.Input( 

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

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

630 "fast lookup."), 

631 name="{skyWcsName}SkyWcsCatalog", 

632 storageClass="ExposureCatalog", 

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

634 ) 

635 externalPhotoCalibTractCatalog = cT.Input( 

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

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

638 name="{photoCalibName}PhotoCalibCatalog", 

639 storageClass="ExposureCatalog", 

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

641 ) 

642 externalPhotoCalibGlobalCatalog = cT.Input( 

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

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

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

646 name="{photoCalibName}PhotoCalibCatalog", 

647 storageClass="ExposureCatalog", 

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

649 ) 

650 finalizedPsfApCorrCatalog = cT.Input( 

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

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

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

654 name="finalized_psf_ap_corr_catalog", 

655 storageClass="ExposureCatalog", 

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

657 ) 

658 measCat = cT.Output( 

659 doc="Output forced photometry catalog.", 

660 name="forced_src_diaObject", 

661 storageClass="SourceCatalog", 

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

663 ) 

664 outputSchema = cT.InitOutput( 

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

666 name="forced_src_diaObject_schema", 

667 storageClass="SourceCatalog", 

668 ) 

669 

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

671 super().__init__(config=config) 

672 if not config.doApplySkyCorr: 

673 self.inputs.remove("skyCorr") 

674 if config.doApplyExternalSkyWcs: 

675 if config.useGlobalExternalSkyWcs: 

676 self.inputs.remove("externalSkyWcsTractCatalog") 

677 else: 

678 self.inputs.remove("externalSkyWcsGlobalCatalog") 

679 else: 

680 self.inputs.remove("externalSkyWcsTractCatalog") 

681 self.inputs.remove("externalSkyWcsGlobalCatalog") 

682 if config.doApplyExternalPhotoCalib: 

683 if config.useGlobalExternalPhotoCalib: 

684 self.inputs.remove("externalPhotoCalibTractCatalog") 

685 else: 

686 self.inputs.remove("externalPhotoCalibGlobalCatalog") 

687 else: 

688 self.inputs.remove("externalPhotoCalibTractCatalog") 

689 self.inputs.remove("externalPhotoCalibGlobalCatalog") 

690 if not config.doApplyFinalizedPsf: 

691 self.inputs.remove("finalizedPsfApCorrCatalog") 

692 

693 

694class ForcedPhotCcdFromDataFrameConfig(ForcedPhotCcdConfig, 

695 pipelineConnections=ForcedPhotCcdFromDataFrameConnections): 

696 def setDefaults(self): 

697 super().setDefaults() 

698 self.footprintSource = "psf" 

699 self.measurement.doReplaceWithNoise = False 

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

701 "base_TransformedCentroidFromCoord", "base_PsfFlux", 

702 "base_PixelFlags"] 

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

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

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

706 self.measurement.slots.shape = None 

707 

708 def validate(self): 

709 super().validate() 

710 if self.footprintSource == "transformed": 

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

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

713 

714 

715class ForcedPhotCcdFromDataFrameTask(ForcedPhotCcdTask): 

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

717 

718 Uses input from a DataFrame instead of SourceCatalog 

719 like the base class ForcedPhotCcd does. 

720 Writes out a SourceCatalog so that the downstream 

721 WriteForcedSourceTableTask can be reused with output from this Task. 

722 """ 

723 _DefaultName = "forcedPhotCcdFromDataFrame" 

724 ConfigClass = ForcedPhotCcdFromDataFrameConfig 

725 

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

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

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

729 

730 if butler is not None: 

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

732 category=FutureWarning, stacklevel=2) 

733 butler = None 

734 

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

736 

737 if self.config.doApCorr: 

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

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

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

741 

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

743 inputs = butlerQC.get(inputRefs) 

744 

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

746 inputs['refWcs'] = None 

747 

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

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

750 if self.config.useGlobalExternalSkyWcs: 

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

752 else: 

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

754 if self.config.useGlobalExternalPhotoCalib: 

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

756 else: 

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

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

759 

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

761 inputs['exposure'], 

762 skyCorr=skyCorr, 

763 externalSkyWcsCatalog=externalSkyWcsCatalog, 

764 externalPhotoCalibCatalog=externalPhotoCalibCatalog, 

765 finalizedPsfApCorrCatalog=finalizedPsfApCorrCatalog 

766 ) 

767 

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

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

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

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

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

773 inputs['refCat'] = refCat 

774 # generateMeasCat does not use the refWcs. 

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

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

777 "visit_detector") 

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

779 # supported in the DataFrame-backed task. 

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

781 outputs = self.run(**inputs) 

782 

783 butlerQC.put(outputs, outputRefs) 

784 else: 

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

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

787 

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

789 """Convert list of DataFrames to reference catalog 

790 

791 Concatenate list of DataFrames presumably from multiple patches and 

792 downselect rows that overlap the exposureBBox using the exposureWcs. 

793 

794 Parameters 

795 ---------- 

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

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

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

799 exposureBBox : `lsst.geom.Box2I` 

800 Bounding box on which to select rows that overlap 

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

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

803 pixel coords with which to compare with exposureBBox 

804 

805 Returns 

806 ------- 

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

808 Source Catalog with minimal schema that overlaps exposureBBox 

809 """ 

810 df = pd.concat(dfList) 

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

812 # to down select rows that overlap the detector bbox 

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

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

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

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

817 return refCat 

818 

819 def df2SourceCat(self, df): 

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

821 

822 The forced measurement subtask expects this as input. 

823 

824 Parameters 

825 ---------- 

826 df : `pandas.DataFrame` 

827 DiaObjects with locations and ids. 

828 

829 Returns 

830 ------- 

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

832 Output catalog with minimal schema. 

833 """ 

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

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

836 outputCatalog.reserve(len(df)) 

837 

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

839 outputRecord = outputCatalog.addNew() 

840 outputRecord.setId(diaObjectId) 

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

842 return outputCatalog