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

211 statements  

« prev     ^ index     » next       coverage.py v7.5.1, created at 2024-05-16 03:37 -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 pandas as pd 

23import numpy as np 

24 

25import lsst.pex.config 

26import lsst.pex.exceptions 

27import lsst.pipe.base 

28import lsst.geom 

29import lsst.afw.detection 

30import lsst.afw.geom 

31import lsst.afw.image 

32import lsst.afw.table 

33import lsst.sphgeom 

34 

35from lsst.pipe.base import PipelineTaskConnections 

36import lsst.pipe.base.connectionTypes as cT 

37 

38import lsst.pipe.base as pipeBase 

39from lsst.skymap import BaseSkyMap 

40 

41from .forcedMeasurement import ForcedMeasurementTask 

42from .applyApCorr import ApplyApCorrTask 

43from .catalogCalculation import CatalogCalculationTask 

44from ._id_generator import DetectorVisitIdGeneratorConfig 

45 

46__all__ = ("ForcedPhotCcdConfig", "ForcedPhotCcdTask", 

47 "ForcedPhotCcdFromDataFrameTask", "ForcedPhotCcdFromDataFrameConfig") 

48 

49 

50class ForcedPhotCcdConnections(PipelineTaskConnections, 

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

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

53 "inputName": "calexp"}): 

54 inputSchema = cT.InitInput( 

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

56 name="{inputCoaddName}Coadd_ref_schema", 

57 storageClass="SourceCatalog", 

58 ) 

59 outputSchema = cT.InitOutput( 

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

61 name="forced_src_schema", 

62 storageClass="SourceCatalog", 

63 ) 

64 exposure = cT.Input( 

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

66 name="{inputName}", 

67 storageClass="ExposureF", 

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

69 ) 

70 refCat = cT.Input( 

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

72 name="{inputCoaddName}Coadd_ref", 

73 storageClass="SourceCatalog", 

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

75 multiple=True, 

76 deferLoad=True, 

77 ) 

78 skyMap = cT.Input( 

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

80 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME, 

81 storageClass="SkyMap", 

82 dimensions=["skymap"], 

83 ) 

84 skyCorr = cT.Input( 

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

86 name="skyCorr", 

87 storageClass="Background", 

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

89 ) 

90 visitSummary = cT.Input( 

91 doc="Input visit-summary catalog with updated calibration objects.", 

92 name="finalVisitSummary", 

93 storageClass="ExposureCatalog", 

94 dimensions=("instrument", "visit"), 

95 ) 

96 measCat = cT.Output( 

97 doc="Output forced photometry catalog.", 

98 name="forced_src", 

99 storageClass="SourceCatalog", 

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

101 ) 

102 

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

104 super().__init__(config=config) 

105 if not config.doApplySkyCorr: 

106 self.inputs.remove("skyCorr") 

107 

108 

109class ForcedPhotCcdConfig(pipeBase.PipelineTaskConfig, 

110 pipelineConnections=ForcedPhotCcdConnections): 

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

112 measurement = lsst.pex.config.ConfigurableField( 

113 target=ForcedMeasurementTask, 

114 doc="subtask to do forced measurement" 

115 ) 

116 coaddName = lsst.pex.config.Field( 

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

118 dtype=str, 

119 default="deep", 

120 ) 

121 doApCorr = lsst.pex.config.Field( 

122 dtype=bool, 

123 default=True, 

124 doc="Run subtask to apply aperture corrections" 

125 ) 

126 applyApCorr = lsst.pex.config.ConfigurableField( 

127 target=ApplyApCorrTask, 

128 doc="Subtask to apply aperture corrections" 

129 ) 

130 catalogCalculation = lsst.pex.config.ConfigurableField( 

131 target=CatalogCalculationTask, 

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

133 ) 

134 doApplySkyCorr = lsst.pex.config.Field( 

135 dtype=bool, 

136 default=False, 

137 doc="Apply sky correction?", 

138 ) 

139 includePhotoCalibVar = lsst.pex.config.Field( 

140 dtype=bool, 

141 default=False, 

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

143 ) 

144 footprintSource = lsst.pex.config.ChoiceField( 

145 dtype=str, 

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

147 allowed={ 

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

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

150 "HeavyFootprints)."), 

151 }, 

152 optional=True, 

153 default="transformed", 

154 ) 

155 psfFootprintScaling = lsst.pex.config.Field( 

156 dtype=float, 

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

158 default=3.0, 

159 ) 

160 idGenerator = DetectorVisitIdGeneratorConfig.make_field() 

161 

162 def setDefaults(self): 

163 # Docstring inherited. 

164 super().setDefaults() 

165 # Footprints here will not be entirely correct, so don't try to make 

166 # a biased correction for blended neighbors. 

167 self.measurement.doReplaceWithNoise = False 

168 # Only run a minimal set of plugins, as these measurements are only 

169 # needed for PSF-like sources. 

170 self.measurement.plugins.names = ["base_PixelFlags", 

171 "base_TransformedCentroid", 

172 "base_PsfFlux", 

173 "base_LocalBackground", 

174 "base_LocalPhotoCalib", 

175 "base_LocalWcs", 

176 ] 

177 self.measurement.slots.shape = None 

178 # Keep track of which footprints contain streaks 

179 self.measurement.plugins['base_PixelFlags'].masksFpAnywhere = ['STREAK'] 

180 self.measurement.plugins['base_PixelFlags'].masksFpCenter = ['STREAK'] 

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

182 # by default in ForcedMeasurementTask. 

183 self.catalogCalculation.plugins.names = [] 

184 

185 

186class ForcedPhotCcdTask(pipeBase.PipelineTask): 

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

188 

189 Parameters 

190 ---------- 

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

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

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

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

195 initInputs : `dict` 

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

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

198 **kwargs 

199 Keyword arguments are passed to the supertask constructor. 

200 """ 

201 

202 ConfigClass = ForcedPhotCcdConfig 

203 _DefaultName = "forcedPhotCcd" 

204 dataPrefix = "" 

205 

206 def __init__(self, refSchema=None, initInputs=None, **kwargs): 

207 super().__init__(**kwargs) 

208 

209 if initInputs is not None: 

210 refSchema = initInputs['inputSchema'].schema 

211 

212 if refSchema is None: 

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

214 

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

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

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

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

219 if self.config.doApCorr: 

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

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

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

223 

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

225 inputs = butlerQC.get(inputRefs) 

226 

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

228 skyMap = inputs.pop('skyMap') 

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

230 

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

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

233 

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

235 inputs['exposure'], 

236 skyCorr=skyCorr, 

237 visitSummary=inputs.pop("visitSummary"), 

238 ) 

239 

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

241 inputs['refWcs']) 

242 

243 if inputs['refCat'] is None: 

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

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

246 else: 

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

248 inputs['exposure'], 

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

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

251 outputs = self.run(**inputs) 

252 butlerQC.put(outputs, outputRefs) 

253 

254 def prepareCalibratedExposure(self, exposure, skyCorr=None, visitSummary=None): 

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

256 and sky corrections if so configured. 

257 

258 Parameters 

259 ---------- 

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

261 Input exposure to adjust calibrations. 

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

263 Sky correction frame to apply if doApplySkyCorr=True. 

264 visitSummary : `lsst.afw.table.ExposureCatalog`, optional 

265 Exposure catalog with update calibrations; any not-None calibration 

266 objects attached will be used. These are applied first and may be 

267 overridden by other arguments. 

268 

269 Returns 

270 ------- 

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

272 Exposure with adjusted calibrations. 

273 """ 

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

275 

276 if visitSummary is not None: 

277 row = visitSummary.find(detectorId) 

278 if row is None: 

279 raise RuntimeError(f"Detector id {detectorId} not found in visitSummary.") 

280 if (photoCalib := row.getPhotoCalib()) is not None: 

281 exposure.setPhotoCalib(photoCalib) 

282 if (skyWcs := row.getWcs()) is not None: 

283 exposure.setWcs(skyWcs) 

284 if (psf := row.getPsf()) is not None: 

285 exposure.setPsf(psf) 

286 if (apCorrMap := row.getApCorrMap()) is not None: 

287 exposure.info.setApCorrMap(apCorrMap) 

288 

289 if skyCorr is not None: 

290 exposure.maskedImage -= skyCorr.getImage() 

291 

292 return exposure 

293 

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

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

296 boundaries of the exposure. 

297 

298 Parameters 

299 ---------- 

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

301 Exposure to generate the catalog for. 

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

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

304 photometry. 

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

306 Reference world coordinate system. 

307 

308 Returns 

309 ------- 

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

311 Filtered catalog of forced sources to measure. 

312 

313 Notes 

314 ----- 

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

316 lsst.meas.algorithms.loadReferenceObjects.ReferenceObjectLoader 

317 

318 """ 

319 mergedRefCat = None 

320 

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

322 # be performed on. 

323 expWcs = exposure.getWcs() 

324 if expWcs is None: 

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

326 else: 

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

328 expBBox = lsst.geom.Box2D(expRegion) 

329 expBoxCorners = expBBox.getCorners() 

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

331 corner in expBoxCorners] 

332 expPolygon = lsst.sphgeom.ConvexPolygon(expSkyCorners) 

333 

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

335 # not contained within the exposure boundaries, or whose 

336 # parents are not within the exposure boundaries. Note 

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

338 # appear before the children. 

339 for refCat in refCats: 

340 refCat = refCat.get() 

341 if mergedRefCat is None: 

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

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

344 for record in refCat: 

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

346 in containedIds): 

347 record.setFootprint(record.getFootprint()) 

348 mergedRefCat.append(record) 

349 containedIds.add(record.getId()) 

350 if mergedRefCat is None: 

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

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

353 return mergedRefCat 

354 

355 def generateMeasCat(self, dataId, exposure, refCat, refWcs): 

356 """Generate a measurement catalog. 

357 

358 Parameters 

359 ---------- 

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

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

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

363 Exposure to generate the catalog for. 

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

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

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

367 Reference world coordinate system. 

368 This parameter is not currently used. 

369 

370 Returns 

371 ------- 

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

373 Catalog of forced sources to measure. 

374 expId : `int` 

375 Unique binary id associated with the input exposure 

376 """ 

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

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

379 idFactory=id_generator.make_table_id_factory()) 

380 return measCat, id_generator.catalog_id 

381 

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

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

384 

385 Parameters 

386 ---------- 

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

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

389 reference catalog. 

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

391 The measurement image upon which to perform forced detection. 

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

393 The reference catalog of sources to measure. 

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

395 The WCS for the references. 

396 exposureId : `int` 

397 Optional unique exposureId used for random seed in measurement 

398 task. 

399 

400 Returns 

401 ------- 

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

403 Structure with fields: 

404 

405 ``measCat`` 

406 Catalog of forced measurement results 

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

408 """ 

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

410 if self.config.doApCorr: 

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

412 if apCorrMap is None: 

413 self.log.warning("Forced exposure image does not have valid aperture correction; skipping.") 

414 else: 

415 self.applyApCorr.run( 

416 catalog=measCat, 

417 apCorrMap=apCorrMap, 

418 ) 

419 self.catalogCalculation.run(measCat) 

420 

421 return pipeBase.Struct(measCat=measCat) 

422 

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

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

425 

426 Notes 

427 ----- 

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

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

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

431 

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

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

434 

435 This default implementation transforms depends on the 

436 ``footprintSource`` configuration parameter. 

437 """ 

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

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

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

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

442 scaling=self.config.psfFootprintScaling) 

443 

444 

445class ForcedPhotCcdFromDataFrameConnections(PipelineTaskConnections, 

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

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

448 "inputName": "calexp", 

449 }): 

450 refCat = cT.Input( 

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

452 name="{inputCoaddName}Diff_fullDiaObjTable", 

453 storageClass="DataFrame", 

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

455 multiple=True, 

456 deferLoad=True, 

457 ) 

458 exposure = cT.Input( 

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

460 name="{inputName}", 

461 storageClass="ExposureF", 

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

463 ) 

464 skyCorr = cT.Input( 

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

466 name="skyCorr", 

467 storageClass="Background", 

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

469 ) 

470 visitSummary = cT.Input( 

471 doc="Input visit-summary catalog with updated calibration objects.", 

472 name="finalVisitSummary", 

473 storageClass="ExposureCatalog", 

474 dimensions=("instrument", "visit"), 

475 ) 

476 skyMap = cT.Input( 

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

478 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME, 

479 storageClass="SkyMap", 

480 dimensions=["skymap"], 

481 ) 

482 measCat = cT.Output( 

483 doc="Output forced photometry catalog.", 

484 name="forced_src_diaObject", 

485 storageClass="SourceCatalog", 

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

487 ) 

488 outputSchema = cT.InitOutput( 

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

490 name="forced_src_diaObject_schema", 

491 storageClass="SourceCatalog", 

492 ) 

493 

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

495 super().__init__(config=config) 

496 if not config.doApplySkyCorr: 

497 self.inputs.remove("skyCorr") 

498 

499 

500class ForcedPhotCcdFromDataFrameConfig(ForcedPhotCcdConfig, 

501 pipelineConnections=ForcedPhotCcdFromDataFrameConnections): 

502 def setDefaults(self): 

503 super().setDefaults() 

504 self.footprintSource = "psf" 

505 self.measurement.doReplaceWithNoise = False 

506 # Only run a minimal set of plugins, as these measurements are only 

507 # needed for PSF-like sources. 

508 self.measurement.plugins.names = ["base_PixelFlags", 

509 "base_TransformedCentroidFromCoord", 

510 "base_PsfFlux", 

511 "base_LocalBackground", 

512 "base_LocalPhotoCalib", 

513 "base_LocalWcs", 

514 ] 

515 self.measurement.slots.shape = None 

516 # Keep track of which footprints contain streaks 

517 self.measurement.plugins['base_PixelFlags'].masksFpAnywhere = ['STREAK'] 

518 self.measurement.plugins['base_PixelFlags'].masksFpCenter = ['STREAK'] 

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

520 # by default in ForcedMeasurementTask. 

521 self.catalogCalculation.plugins.names = [] 

522 

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

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

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

526 

527 def validate(self): 

528 super().validate() 

529 if self.footprintSource == "transformed": 

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

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

532 

533 

534class ForcedPhotCcdFromDataFrameTask(ForcedPhotCcdTask): 

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

536 

537 Uses input from a DataFrame instead of SourceCatalog 

538 like the base class ForcedPhotCcd does. 

539 Writes out a SourceCatalog so that the downstream 

540 WriteForcedSourceTableTask can be reused with output from this Task. 

541 """ 

542 _DefaultName = "forcedPhotCcdFromDataFrame" 

543 ConfigClass = ForcedPhotCcdFromDataFrameConfig 

544 

545 def __init__(self, refSchema=None, initInputs=None, **kwargs): 

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

547 pipeBase.PipelineTask.__init__(self, **kwargs) 

548 

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

550 

551 if self.config.doApCorr: 

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

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

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

555 

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

557 inputs = butlerQC.get(inputRefs) 

558 

559 tract = butlerQC.quantum.dataId["tract"] 

560 skyMap = inputs.pop("skyMap") 

561 inputs["refWcs"] = skyMap[tract].getWcs() 

562 

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

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

565 

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

567 inputs['exposure'], 

568 skyCorr=skyCorr, 

569 visitSummary=inputs.pop("visitSummary"), 

570 ) 

571 

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

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

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

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

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

577 inputs['refCat'] = refCat 

578 # generateMeasCat does not use the refWcs. 

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

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

581 ) 

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

583 # supported in the DataFrame-backed task. 

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

585 outputs = self.run(**inputs) 

586 

587 butlerQC.put(outputs, outputRefs) 

588 else: 

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

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

591 

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

593 """Convert list of DataFrames to reference catalog 

594 

595 Concatenate list of DataFrames presumably from multiple patches and 

596 downselect rows that overlap the exposureBBox using the exposureWcs. 

597 

598 Parameters 

599 ---------- 

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

601 Each element containst diaObjects with ra/dec position in degrees 

602 Columns 'diaObjectId', 'ra', 'dec' are expected 

603 exposureBBox : `lsst.geom.Box2I` 

604 Bounding box on which to select rows that overlap 

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

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

607 pixel coords with which to compare with exposureBBox 

608 

609 Returns 

610 ------- 

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

612 Source Catalog with minimal schema that overlaps exposureBBox 

613 """ 

614 df = pd.concat(dfList) 

615 # translate ra/dec coords in dataframe to detector pixel coords 

616 # to down select rows that overlap the detector bbox 

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

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

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

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

621 return refCat 

622 

623 def df2SourceCat(self, df): 

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

625 

626 The forced measurement subtask expects this as input. 

627 

628 Parameters 

629 ---------- 

630 df : `pandas.DataFrame` 

631 DiaObjects with locations and ids. 

632 

633 Returns 

634 ------- 

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

636 Output catalog with minimal schema. 

637 """ 

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

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

640 outputCatalog.reserve(len(df)) 

641 

642 for diaObjectId, ra, dec in df[['ra', 'dec']].itertuples(): 

643 outputRecord = outputCatalog.addNew() 

644 outputRecord.setId(diaObjectId) 

645 outputRecord.setCoord(lsst.geom.SpherePoint(ra, dec, lsst.geom.degrees)) 

646 return outputCatalog