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

275 statements  

« prev     ^ index     » next       coverage.py v7.2.4, created at 2023-04-30 02:10 -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.pipe.base import PipelineTaskConnections 

38import lsst.pipe.base.connectionTypes as cT 

39 

40import lsst.pipe.base as pipeBase 

41from lsst.skymap import BaseSkyMap 

42 

43from .forcedMeasurement import ForcedMeasurementTask 

44from .applyApCorr import ApplyApCorrTask 

45from .catalogCalculation import CatalogCalculationTask 

46from ._id_generator import DetectorVisitIdGeneratorConfig 

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": "gbdesAstrometricFit", 

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="finalVisitSummary", 

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="finalVisitSummary", 

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=True, 

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 idGenerator = DetectorVisitIdGeneratorConfig.make_field() 

252 

253 def setDefaults(self): 

254 # Docstring inherited. 

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

256 # by default in ForcedMeasurementTask. 

257 super().setDefaults() 

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

259 self.catalogCalculation.plugins.names = [] 

260 

261 

262class ForcedPhotCcdTask(pipeBase.PipelineTask): 

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

264 

265 Parameters 

266 ---------- 

267 butler : `None` 

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

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

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

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

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

273 initInputs : `dict` 

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

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

276 **kwds 

277 Keyword arguments are passed to the supertask constructor. 

278 """ 

279 

280 ConfigClass = ForcedPhotCcdConfig 

281 _DefaultName = "forcedPhotCcd" 

282 dataPrefix = "" 

283 

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

285 super().__init__(**kwds) 

286 

287 if butler is not None: 

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

289 category=FutureWarning, stacklevel=2) 

290 butler = None 

291 

292 if initInputs is not None: 

293 refSchema = initInputs['inputSchema'].schema 

294 

295 if refSchema is None: 

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

297 

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

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

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

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

302 if self.config.doApCorr: 

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

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

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

306 

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

308 inputs = butlerQC.get(inputRefs) 

309 

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

311 skyMap = inputs.pop('skyMap') 

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

313 

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

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

316 if self.config.useGlobalExternalSkyWcs: 

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

318 else: 

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

320 if self.config.useGlobalExternalPhotoCalib: 

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

322 else: 

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

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

325 

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

327 inputs['exposure'], 

328 skyCorr=skyCorr, 

329 externalSkyWcsCatalog=externalSkyWcsCatalog, 

330 externalPhotoCalibCatalog=externalPhotoCalibCatalog, 

331 finalizedPsfApCorrCatalog=finalizedPsfApCorrCatalog 

332 ) 

333 

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

335 inputs['refWcs']) 

336 

337 if inputs['refCat'] is None: 

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

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

340 else: 

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

342 inputs['exposure'], 

343 inputs['refCat'], inputs['refWcs']) 

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, dataId, exposure, refCat, refWcs): 

487 """Generate a measurement catalog. 

488 

489 Parameters 

490 ---------- 

491 dataId : `lsst.daf.butler.DataCoordinate` 

492 Butler data ID for this image, with ``{visit, detector}`` keys. 

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 

501 Returns 

502 ------- 

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

504 Catalog of forced sources to measure. 

505 expId : `int` 

506 Unique binary id associated with the input exposure 

507 """ 

508 id_generator = self.config.idGenerator.apply(dataId) 

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

510 idFactory=id_generator.make_table_id_factory()) 

511 return measCat, id_generator.catalog_id 

512 

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

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

515 

516 Parameters 

517 ---------- 

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

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

520 reference catalog. 

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

522 The measurement image upon which to perform forced detection. 

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

524 The reference catalog of sources to measure. 

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

526 The WCS for the references. 

527 exposureId : `int` 

528 Optional unique exposureId used for random seed in measurement 

529 task. 

530 

531 Returns 

532 ------- 

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

534 Structure with fields: 

535 

536 ``measCat`` 

537 Catalog of forced measurement results 

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

539 """ 

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

541 if self.config.doApCorr: 

542 self.applyApCorr.run( 

543 catalog=measCat, 

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

545 ) 

546 self.catalogCalculation.run(measCat) 

547 

548 return pipeBase.Struct(measCat=measCat) 

549 

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

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

552 

553 Notes 

554 ----- 

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

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

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

558 

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

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

561 

562 This default implementation transforms depends on the 

563 ``footprintSource`` configuration parameter. 

564 """ 

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

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

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

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

569 scaling=self.config.psfFootprintScaling) 

570 

571 

572class ForcedPhotCcdFromDataFrameConnections(PipelineTaskConnections, 

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

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

575 "inputName": "calexp", 

576 "skyWcsName": "gbdesAstrometricFit", 

577 "photoCalibName": "fgcm"}): 

578 refCat = cT.Input( 

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

580 name="{inputCoaddName}Diff_fullDiaObjTable", 

581 storageClass="DataFrame", 

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

583 multiple=True, 

584 deferLoad=True, 

585 ) 

586 exposure = cT.Input( 

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

588 name="{inputName}", 

589 storageClass="ExposureF", 

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

591 ) 

592 skyCorr = cT.Input( 

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

594 name="skyCorr", 

595 storageClass="Background", 

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

597 ) 

598 externalSkyWcsTractCatalog = cT.Input( 

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

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

601 name="{skyWcsName}SkyWcsCatalog", 

602 storageClass="ExposureCatalog", 

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

604 ) 

605 externalSkyWcsGlobalCatalog = cT.Input( 

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

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

608 "fast lookup."), 

609 name="{skyWcsName}SkyWcsCatalog", 

610 storageClass="ExposureCatalog", 

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

612 ) 

613 externalPhotoCalibTractCatalog = cT.Input( 

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

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

616 name="{photoCalibName}PhotoCalibCatalog", 

617 storageClass="ExposureCatalog", 

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

619 ) 

620 externalPhotoCalibGlobalCatalog = cT.Input( 

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

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

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

624 name="{photoCalibName}PhotoCalibCatalog", 

625 storageClass="ExposureCatalog", 

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

627 ) 

628 finalizedPsfApCorrCatalog = cT.Input( 

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

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

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

632 name="finalized_psf_ap_corr_catalog", 

633 storageClass="ExposureCatalog", 

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

635 ) 

636 measCat = cT.Output( 

637 doc="Output forced photometry catalog.", 

638 name="forced_src_diaObject", 

639 storageClass="SourceCatalog", 

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

641 ) 

642 outputSchema = cT.InitOutput( 

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

644 name="forced_src_diaObject_schema", 

645 storageClass="SourceCatalog", 

646 ) 

647 

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

649 super().__init__(config=config) 

650 if not config.doApplySkyCorr: 

651 self.inputs.remove("skyCorr") 

652 if config.doApplyExternalSkyWcs: 

653 if config.useGlobalExternalSkyWcs: 

654 self.inputs.remove("externalSkyWcsTractCatalog") 

655 else: 

656 self.inputs.remove("externalSkyWcsGlobalCatalog") 

657 else: 

658 self.inputs.remove("externalSkyWcsTractCatalog") 

659 self.inputs.remove("externalSkyWcsGlobalCatalog") 

660 if config.doApplyExternalPhotoCalib: 

661 if config.useGlobalExternalPhotoCalib: 

662 self.inputs.remove("externalPhotoCalibTractCatalog") 

663 else: 

664 self.inputs.remove("externalPhotoCalibGlobalCatalog") 

665 else: 

666 self.inputs.remove("externalPhotoCalibTractCatalog") 

667 self.inputs.remove("externalPhotoCalibGlobalCatalog") 

668 if not config.doApplyFinalizedPsf: 

669 self.inputs.remove("finalizedPsfApCorrCatalog") 

670 

671 

672class ForcedPhotCcdFromDataFrameConfig(ForcedPhotCcdConfig, 

673 pipelineConnections=ForcedPhotCcdFromDataFrameConnections): 

674 def setDefaults(self): 

675 super().setDefaults() 

676 self.footprintSource = "psf" 

677 self.measurement.doReplaceWithNoise = False 

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

679 "base_TransformedCentroidFromCoord", "base_PsfFlux", 

680 "base_PixelFlags"] 

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

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

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

684 self.measurement.slots.shape = None 

685 

686 def validate(self): 

687 super().validate() 

688 if self.footprintSource == "transformed": 

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

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

691 

692 

693class ForcedPhotCcdFromDataFrameTask(ForcedPhotCcdTask): 

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

695 

696 Uses input from a DataFrame instead of SourceCatalog 

697 like the base class ForcedPhotCcd does. 

698 Writes out a SourceCatalog so that the downstream 

699 WriteForcedSourceTableTask can be reused with output from this Task. 

700 """ 

701 _DefaultName = "forcedPhotCcdFromDataFrame" 

702 ConfigClass = ForcedPhotCcdFromDataFrameConfig 

703 

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

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

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

707 

708 if butler is not None: 

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

710 category=FutureWarning, stacklevel=2) 

711 butler = None 

712 

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

714 

715 if self.config.doApCorr: 

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

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

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

719 

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

721 inputs = butlerQC.get(inputRefs) 

722 

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

724 inputs['refWcs'] = None 

725 

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

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

728 if self.config.useGlobalExternalSkyWcs: 

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

730 else: 

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

732 if self.config.useGlobalExternalPhotoCalib: 

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

734 else: 

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

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

737 

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

739 inputs['exposure'], 

740 skyCorr=skyCorr, 

741 externalSkyWcsCatalog=externalSkyWcsCatalog, 

742 externalPhotoCalibCatalog=externalPhotoCalibCatalog, 

743 finalizedPsfApCorrCatalog=finalizedPsfApCorrCatalog 

744 ) 

745 

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

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

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

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

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

751 inputs['refCat'] = refCat 

752 # generateMeasCat does not use the refWcs. 

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

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

755 ) 

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

757 # supported in the DataFrame-backed task. 

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

759 outputs = self.run(**inputs) 

760 

761 butlerQC.put(outputs, outputRefs) 

762 else: 

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

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

765 

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

767 """Convert list of DataFrames to reference catalog 

768 

769 Concatenate list of DataFrames presumably from multiple patches and 

770 downselect rows that overlap the exposureBBox using the exposureWcs. 

771 

772 Parameters 

773 ---------- 

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

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

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

777 exposureBBox : `lsst.geom.Box2I` 

778 Bounding box on which to select rows that overlap 

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

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

781 pixel coords with which to compare with exposureBBox 

782 

783 Returns 

784 ------- 

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

786 Source Catalog with minimal schema that overlaps exposureBBox 

787 """ 

788 df = pd.concat(dfList) 

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

790 # to down select rows that overlap the detector bbox 

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

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

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

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

795 return refCat 

796 

797 def df2SourceCat(self, df): 

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

799 

800 The forced measurement subtask expects this as input. 

801 

802 Parameters 

803 ---------- 

804 df : `pandas.DataFrame` 

805 DiaObjects with locations and ids. 

806 

807 Returns 

808 ------- 

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

810 Output catalog with minimal schema. 

811 """ 

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

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

814 outputCatalog.reserve(len(df)) 

815 

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

817 outputRecord = outputCatalog.addNew() 

818 outputRecord.setId(diaObjectId) 

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

820 return outputCatalog