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

383 statements  

« prev     ^ index     » next       coverage.py v6.4.1, created at 2022-06-28 02:19 -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 collections 

23import logging 

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 .references import MultiBandReferencesTask 

45from .forcedMeasurement import ForcedMeasurementTask 

46from .applyApCorr import ApplyApCorrTask 

47from .catalogCalculation import CatalogCalculationTask 

48 

49try: 

50 from lsst.meas.mosaic import applyMosaicResults 

51except ImportError: 

52 applyMosaicResults = None 

53 

54__all__ = ("PerTractCcdDataIdContainer", "ForcedPhotCcdConfig", "ForcedPhotCcdTask", "imageOverlapsTract", 

55 "ForcedPhotCcdFromDataFrameTask", "ForcedPhotCcdFromDataFrameConfig") 

56 

57 

58class PerTractCcdDataIdContainer(pipeBase.DataIdContainer): 

59 """A data ID container which combines raw data IDs with a tract. 

60 

61 Notes 

62 ----- 

63 Required because we need to add "tract" to the raw data ID keys (defined as 

64 whatever we use for ``src``) when no tract is provided (so that the user is 

65 not required to know which tracts are spanned by the raw data ID). 

66 

67 This subclass of `~lsst.pipe.base.DataIdContainer` assumes that a calexp is 

68 being measured using the detection information, a set of reference 

69 catalogs, from the set of coadds which intersect with the calexp. It needs 

70 the calexp id (e.g. visit, raft, sensor), but is also uses the tract to 

71 decide what set of coadds to use. The references from the tract whose 

72 patches intersect with the calexp are used. 

73 """ 

74 

75 def makeDataRefList(self, namespace): 

76 """Make self.refList from self.idList 

77 """ 

78 if self.datasetType is None: 

79 raise RuntimeError("Must call setDatasetType first") 

80 log = logging.getLogger(__name__).getChild("PerTractCcdDataIdContainer") 

81 skymap = None 

82 visitTract = collections.defaultdict(set) # Set of tracts for each visit 

83 visitRefs = collections.defaultdict(list) # List of data references for each visit 

84 for dataId in self.idList: 

85 if "tract" not in dataId: 

86 # Discover which tracts the data overlaps 

87 log.info("Reading WCS for components of dataId=%s to determine tracts", dict(dataId)) 

88 if skymap is None: 

89 skymap = namespace.butler.get(namespace.config.coaddName + "Coadd_skyMap") 

90 

91 for ref in namespace.butler.subset("calexp", dataId=dataId): 

92 if not ref.datasetExists("calexp"): 

93 continue 

94 

95 visit = ref.dataId["visit"] 

96 visitRefs[visit].append(ref) 

97 

98 md = ref.get("calexp_md", immediate=True) 

99 wcs = lsst.afw.geom.makeSkyWcs(md) 

100 box = lsst.geom.Box2D(lsst.afw.image.bboxFromMetadata(md)) 

101 # Going with just the nearest tract. Since we're throwing all tracts for the visit 

102 # together, this shouldn't be a problem unless the tracts are much smaller than a CCD. 

103 tract = skymap.findTract(wcs.pixelToSky(box.getCenter())) 

104 if imageOverlapsTract(tract, wcs, box): 

105 visitTract[visit].add(tract.getId()) 

106 else: 

107 self.refList.extend(ref for ref in namespace.butler.subset(self.datasetType, dataId=dataId)) 

108 

109 # Ensure all components of a visit are kept together by putting them all in the same set of tracts 

110 for visit, tractSet in visitTract.items(): 

111 for ref in visitRefs[visit]: 

112 for tract in tractSet: 

113 self.refList.append(namespace.butler.dataRef(datasetType=self.datasetType, 

114 dataId=ref.dataId, tract=tract)) 

115 if visitTract: 

116 tractCounter = collections.Counter() 

117 for tractSet in visitTract.values(): 

118 tractCounter.update(tractSet) 

119 log.info("Number of visits for each tract: %s", dict(tractCounter)) 

120 

121 

122def imageOverlapsTract(tract, imageWcs, imageBox): 

123 """Return whether the given bounding box overlaps the tract given a WCS. 

124 

125 Parameters 

126 ---------- 

127 tract : `lsst.skymap.TractInfo` 

128 TractInfo specifying a tract. 

129 imageWcs : `lsst.afw.geom.SkyWcs` 

130 World coordinate system for the image. 

131 imageBox : `lsst.geom.Box2I` 

132 Bounding box for the image. 

133 

134 Returns 

135 ------- 

136 overlap : `bool` 

137 `True` if the bounding box overlaps the tract; `False` otherwise. 

138 """ 

139 tractPoly = tract.getOuterSkyPolygon() 

140 

141 imagePixelCorners = lsst.geom.Box2D(imageBox).getCorners() 

142 try: 

143 imageSkyCorners = imageWcs.pixelToSky(imagePixelCorners) 

144 except lsst.pex.exceptions.LsstCppException as e: 

145 # Protecting ourselves from awful Wcs solutions in input images 

146 if (not isinstance(e.message, lsst.pex.exceptions.DomainErrorException) 

147 and not isinstance(e.message, lsst.pex.exceptions.RuntimeErrorException)): 

148 raise 

149 return False 

150 

151 imagePoly = lsst.sphgeom.ConvexPolygon.convexHull([coord.getVector() for coord in imageSkyCorners]) 

152 return tractPoly.intersects(imagePoly) # "intersects" also covers "contains" or "is contained by" 

153 

154 

155class ForcedPhotCcdConnections(PipelineTaskConnections, 

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

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

158 "inputName": "calexp", 

159 "skyWcsName": "jointcal", 

160 "photoCalibName": "fgcm"}): 

161 inputSchema = cT.InitInput( 

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

163 name="{inputCoaddName}Coadd_ref_schema", 

164 storageClass="SourceCatalog", 

165 ) 

166 outputSchema = cT.InitOutput( 

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

168 name="forced_src_schema", 

169 storageClass="SourceCatalog", 

170 ) 

171 exposure = cT.Input( 

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

173 name="{inputName}", 

174 storageClass="ExposureF", 

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

176 ) 

177 refCat = cT.Input( 

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

179 name="{inputCoaddName}Coadd_ref", 

180 storageClass="SourceCatalog", 

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

182 multiple=True, 

183 deferLoad=True, 

184 ) 

185 skyMap = cT.Input( 

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

187 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME, 

188 storageClass="SkyMap", 

189 dimensions=["skymap"], 

190 ) 

191 skyCorr = cT.Input( 

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

193 name="skyCorr", 

194 storageClass="Background", 

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

196 ) 

197 externalSkyWcsTractCatalog = cT.Input( 

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

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

200 name="{skyWcsName}SkyWcsCatalog", 

201 storageClass="ExposureCatalog", 

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

203 ) 

204 externalSkyWcsGlobalCatalog = cT.Input( 

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

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

207 "fast lookup."), 

208 name="{skyWcsName}SkyWcsCatalog", 

209 storageClass="ExposureCatalog", 

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

211 ) 

212 externalPhotoCalibTractCatalog = cT.Input( 

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

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

215 name="{photoCalibName}PhotoCalibCatalog", 

216 storageClass="ExposureCatalog", 

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

218 ) 

219 externalPhotoCalibGlobalCatalog = cT.Input( 

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

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

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

223 name="{photoCalibName}PhotoCalibCatalog", 

224 storageClass="ExposureCatalog", 

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

226 ) 

227 finalizedPsfApCorrCatalog = cT.Input( 

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

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

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

231 name="finalized_psf_ap_corr_catalog", 

232 storageClass="ExposureCatalog", 

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

234 ) 

235 measCat = cT.Output( 

236 doc="Output forced photometry catalog.", 

237 name="forced_src", 

238 storageClass="SourceCatalog", 

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

240 ) 

241 

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

243 super().__init__(config=config) 

244 if not config.doApplySkyCorr: 

245 self.inputs.remove("skyCorr") 

246 if config.doApplyExternalSkyWcs: 

247 if config.useGlobalExternalSkyWcs: 

248 self.inputs.remove("externalSkyWcsTractCatalog") 

249 else: 

250 self.inputs.remove("externalSkyWcsGlobalCatalog") 

251 else: 

252 self.inputs.remove("externalSkyWcsTractCatalog") 

253 self.inputs.remove("externalSkyWcsGlobalCatalog") 

254 if config.doApplyExternalPhotoCalib: 

255 if config.useGlobalExternalPhotoCalib: 

256 self.inputs.remove("externalPhotoCalibTractCatalog") 

257 else: 

258 self.inputs.remove("externalPhotoCalibGlobalCatalog") 

259 else: 

260 self.inputs.remove("externalPhotoCalibTractCatalog") 

261 self.inputs.remove("externalPhotoCalibGlobalCatalog") 

262 if not config.doApplyFinalizedPsf: 

263 self.inputs.remove("finalizedPsfApCorrCatalog") 

264 

265 

266class ForcedPhotCcdConfig(pipeBase.PipelineTaskConfig, 

267 pipelineConnections=ForcedPhotCcdConnections): 

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

269 references = lsst.pex.config.ConfigurableField( 

270 target=MultiBandReferencesTask, 

271 doc="subtask to retrieve reference source catalog" 

272 ) 

273 measurement = lsst.pex.config.ConfigurableField( 

274 target=ForcedMeasurementTask, 

275 doc="subtask to do forced measurement" 

276 ) 

277 coaddName = lsst.pex.config.Field( 

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

279 dtype=str, 

280 default="deep", 

281 ) 

282 doApCorr = lsst.pex.config.Field( 

283 dtype=bool, 

284 default=True, 

285 doc="Run subtask to apply aperture corrections" 

286 ) 

287 applyApCorr = lsst.pex.config.ConfigurableField( 

288 target=ApplyApCorrTask, 

289 doc="Subtask to apply aperture corrections" 

290 ) 

291 catalogCalculation = lsst.pex.config.ConfigurableField( 

292 target=CatalogCalculationTask, 

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

294 ) 

295 doApplyUberCal = lsst.pex.config.Field( 

296 dtype=bool, 

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

298 default=False, 

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

300 ) 

301 doApplyExternalPhotoCalib = lsst.pex.config.Field( 

302 dtype=bool, 

303 default=False, 

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

305 "`lsst.afw.image.PhotoCalib` object. Uses the " 

306 "``externalPhotoCalibName`` field to determine which calibration " 

307 "to load."), 

308 ) 

309 useGlobalExternalPhotoCalib = lsst.pex.config.Field( 

310 dtype=bool, 

311 default=True, 

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

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

314 "calibration files.") 

315 ) 

316 doApplyExternalSkyWcs = lsst.pex.config.Field( 

317 dtype=bool, 

318 default=False, 

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

320 "`lsst.afw.geom.SkyWcs` object. Uses ``externalSkyWcsName`` " 

321 "field to determine which calibration to load."), 

322 ) 

323 useGlobalExternalSkyWcs = lsst.pex.config.Field( 

324 dtype=bool, 

325 default=False, 

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

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

328 "files.") 

329 ) 

330 doApplyFinalizedPsf = lsst.pex.config.Field( 

331 dtype=bool, 

332 default=False, 

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

334 ) 

335 doApplySkyCorr = lsst.pex.config.Field( 

336 dtype=bool, 

337 default=False, 

338 doc="Apply sky correction?", 

339 ) 

340 includePhotoCalibVar = lsst.pex.config.Field( 

341 dtype=bool, 

342 default=False, 

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

344 ) 

345 externalPhotoCalibName = lsst.pex.config.ChoiceField( 

346 dtype=str, 

347 doc=("Type of external PhotoCalib if ``doApplyExternalPhotoCalib`` is True. " 

348 "Unused for Gen3 middleware."), 

349 default="jointcal", 

350 allowed={ 

351 "jointcal": "Use jointcal_photoCalib", 

352 "fgcm": "Use fgcm_photoCalib", 

353 "fgcm_tract": "Use fgcm_tract_photoCalib" 

354 }, 

355 ) 

356 externalSkyWcsName = lsst.pex.config.ChoiceField( 

357 dtype=str, 

358 doc="Type of external SkyWcs if ``doApplyExternalSkyWcs`` is True. Unused for Gen3 middleware.", 

359 default="jointcal", 

360 allowed={ 

361 "jointcal": "Use jointcal_wcs" 

362 }, 

363 ) 

364 footprintSource = lsst.pex.config.ChoiceField( 

365 dtype=str, 

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

367 allowed={ 

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

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

370 "HeavyFootprints)."), 

371 }, 

372 optional=True, 

373 default="transformed", 

374 ) 

375 psfFootprintScaling = lsst.pex.config.Field( 

376 dtype=float, 

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

378 default=3.0, 

379 ) 

380 

381 def setDefaults(self): 

382 # Docstring inherited. 

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

384 # ForcedMeasurementTask 

385 super().setDefaults() 

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

387 self.catalogCalculation.plugins.names = [] 

388 

389 

390class ForcedPhotCcdTask(pipeBase.PipelineTask, pipeBase.CmdLineTask): 

391 """A command-line driver for performing forced measurement on CCD images. 

392 

393 Parameters 

394 ---------- 

395 butler : `lsst.daf.persistence.butler.Butler`, optional 

396 A Butler which will be passed to the references subtask to allow it to 

397 load its schema from disk. Optional, but must be specified if 

398 ``refSchema`` is not; if both are specified, ``refSchema`` takes 

399 precedence. 

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

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

402 references subtask. Optional, but must be specified if ``butler`` is 

403 not; if both are specified, ``refSchema`` takes precedence. 

404 **kwds 

405 Keyword arguments are passed to the supertask constructor. 

406 

407 Notes 

408 ----- 

409 The `runDataRef` method takes a `~lsst.daf.persistence.ButlerDataRef` argument 

410 that corresponds to a single CCD. This should contain the data ID keys that 

411 correspond to the ``forced_src`` dataset (the output dataset for this 

412 task), which are typically all those used to specify the ``calexp`` dataset 

413 (``visit``, ``raft``, ``sensor`` for LSST data) as well as a coadd tract. 

414 The tract is used to look up the appropriate coadd measurement catalogs to 

415 use as references (e.g. ``deepCoadd_src``; see 

416 :lsst-task:`lsst.meas.base.references.CoaddSrcReferencesTask` for more 

417 information). While the tract must be given as part of the dataRef, the 

418 patches are determined automatically from the bounding box and WCS of the 

419 calexp to be measured, and the filter used to fetch references is set via 

420 the ``filter`` option in the configuration of 

421 :lsst-task:`lsst.meas.base.references.BaseReferencesTask`). 

422 """ 

423 

424 ConfigClass = ForcedPhotCcdConfig 

425 RunnerClass = pipeBase.ButlerInitializedTaskRunner 

426 _DefaultName = "forcedPhotCcd" 

427 dataPrefix = "" 

428 

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

430 super().__init__(**kwds) 

431 

432 if initInputs is not None: 

433 refSchema = initInputs['inputSchema'].schema 

434 

435 self.makeSubtask("references", butler=butler, schema=refSchema) 

436 if refSchema is None: 

437 refSchema = self.references.schema 

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

439 # It is necessary to get the schema internal to the forced measurement task until such a time 

440 # that the schema is not owned by the measurement task, but is passed in by an external caller 

441 if self.config.doApCorr: 

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

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

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

445 

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

447 inputs = butlerQC.get(inputRefs) 

448 

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

450 skyMap = inputs.pop('skyMap') 

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

452 

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

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

455 if self.config.useGlobalExternalSkyWcs: 

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

457 else: 

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

459 if self.config.useGlobalExternalPhotoCalib: 

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

461 else: 

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

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

464 

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

466 inputs['exposure'], 

467 skyCorr=skyCorr, 

468 externalSkyWcsCatalog=externalSkyWcsCatalog, 

469 externalPhotoCalibCatalog=externalPhotoCalibCatalog, 

470 finalizedPsfApCorrCatalog=finalizedPsfApCorrCatalog 

471 ) 

472 

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

474 inputs['refWcs']) 

475 

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

477 inputs['exposure'], 

478 inputs['refCat'], inputs['refWcs'], 

479 "visit_detector") 

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

481 outputs = self.run(**inputs) 

482 butlerQC.put(outputs, outputRefs) 

483 

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

485 externalPhotoCalibCatalog=None, finalizedPsfApCorrCatalog=None): 

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

487 and sky corrections if so configured. 

488 

489 Parameters 

490 ---------- 

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

492 Input exposure to adjust calibrations. 

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

494 Sky correction frame to apply if doApplySkyCorr=True. 

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

496 Exposure catalog with external skyWcs to be applied 

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

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

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

500 Exposure catalog with external photoCalib to be applied 

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

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

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

504 Exposure catalog with finalized psf models and aperture correction 

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

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

507 

508 Returns 

509 ------- 

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

511 Exposure with adjusted calibrations. 

512 """ 

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

514 

515 if externalPhotoCalibCatalog is not None: 

516 row = externalPhotoCalibCatalog.find(detectorId) 

517 if row is None: 

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

519 "Using original photoCalib.", detectorId) 

520 else: 

521 photoCalib = row.getPhotoCalib() 

522 if photoCalib is None: 

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

524 "Using original photoCalib.", detectorId) 

525 else: 

526 exposure.setPhotoCalib(photoCalib) 

527 

528 if externalSkyWcsCatalog is not None: 

529 row = externalSkyWcsCatalog.find(detectorId) 

530 if row is None: 

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

532 "Using original skyWcs.", detectorId) 

533 else: 

534 skyWcs = row.getWcs() 

535 if skyWcs is None: 

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

537 "Using original skyWcs.", detectorId) 

538 else: 

539 exposure.setWcs(skyWcs) 

540 

541 if finalizedPsfApCorrCatalog is not None: 

542 row = finalizedPsfApCorrCatalog.find(detectorId) 

543 if row is None: 

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

545 "Using original psf.", detectorId) 

546 else: 

547 psf = row.getPsf() 

548 apCorrMap = row.getApCorrMap() 

549 if psf is None or apCorrMap is None: 

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

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

552 else: 

553 exposure.setPsf(psf) 

554 exposure.setApCorrMap(apCorrMap) 

555 

556 if skyCorr is not None: 

557 exposure.maskedImage -= skyCorr.getImage() 

558 

559 return exposure 

560 

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

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

563 boundaries of the exposure. 

564 

565 Parameters 

566 ---------- 

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

568 Exposure to generate the catalog for. 

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

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

571 photometry. 

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

573 Reference world coordinate system. 

574 

575 Returns 

576 ------- 

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

578 Filtered catalog of forced sources to measure. 

579 

580 Notes 

581 ----- 

582 Filtering the reference catalog is currently handled by Gen2 

583 specific methods. To function for Gen3, this method copies 

584 code segments to do the filtering and transformation. The 

585 majority of this code is based on the methods of 

586 lsst.meas.algorithms.loadReferenceObjects.ReferenceObjectLoader 

587 

588 """ 

589 

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

591 # be performed on. 

592 expWcs = exposure.getWcs() 

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

594 expBBox = lsst.geom.Box2D(expRegion) 

595 expBoxCorners = expBBox.getCorners() 

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

597 corner in expBoxCorners] 

598 expPolygon = lsst.sphgeom.ConvexPolygon(expSkyCorners) 

599 

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

601 # not contained within the exposure boundaries, or whose 

602 # parents are not within the exposure boundaries. Note 

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

604 # appear before the children. 

605 mergedRefCat = None 

606 for refCat in refCats: 

607 refCat = refCat.get() 

608 if mergedRefCat is None: 

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

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

611 for record in refCat: 

612 if expPolygon.contains(record.getCoord().getVector()) and record.getParent() in containedIds: 

613 record.setFootprint(record.getFootprint()) 

614 mergedRefCat.append(record) 

615 containedIds.add(record.getId()) 

616 if mergedRefCat is None: 

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

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

619 return mergedRefCat 

620 

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

622 """Generate a measurement catalog for Gen3. 

623 

624 Parameters 

625 ---------- 

626 exposureDataId : `DataId` 

627 Butler dataId for this exposure. 

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

629 Exposure to generate the catalog for. 

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

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

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

633 Reference world coordinate system. 

634 This parameter is not currently used. 

635 idPackerName : `str` 

636 Type of ID packer to construct from the registry. 

637 

638 Returns 

639 ------- 

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

641 Catalog of forced sources to measure. 

642 expId : `int` 

643 Unique binary id associated with the input exposure 

644 """ 

645 exposureIdInfo = ExposureIdInfo.fromDataId(exposureDataId, idPackerName) 

646 idFactory = exposureIdInfo.makeSourceIdFactory() 

647 

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

649 idFactory=idFactory) 

650 return measCat, exposureIdInfo.expId 

651 

652 def runDataRef(self, dataRef, psfCache=None): 

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

654 

655 Parameters 

656 ---------- 

657 dataRef : `lsst.daf.persistence.ButlerDataRef` 

658 Passed to the ``references`` subtask to obtain the reference WCS, 

659 the ``getExposure`` method (implemented by derived classes) to 

660 read the measurment image, and the ``fetchReferences`` method to 

661 get the exposure and load the reference catalog (see 

662 :lsst-task`lsst.meas.base.references.CoaddSrcReferencesTask`). 

663 Refer to derived class documentation for details of the datasets 

664 and data ID keys which are used. 

665 psfCache : `int`, optional 

666 Size of PSF cache, or `None`. The size of the PSF cache can have 

667 a significant effect upon the runtime for complicated PSF models. 

668 

669 Notes 

670 ----- 

671 Sources are generated with ``generateMeasCat`` in the ``measurement`` 

672 subtask. These are passed to ``measurement``'s ``run`` method, which 

673 fills the source catalog with the forced measurement results. The 

674 sources are then passed to the ``writeOutputs`` method (implemented by 

675 derived classes) which writes the outputs. 

676 """ 

677 refWcs = self.references.getWcs(dataRef) 

678 exposure = self.getExposure(dataRef) 

679 if psfCache is not None: 

680 exposure.getPsf().setCacheSize(psfCache) 

681 refCat = self.fetchReferences(dataRef, exposure) 

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

683 idFactory=self.makeIdFactory(dataRef)) 

684 self.log.info("Performing forced measurement on %s", dataRef.dataId) 

685 self.attachFootprints(measCat, refCat, exposure, refWcs) 

686 

687 exposureId = self.getExposureId(dataRef) 

688 

689 forcedPhotResult = self.run(measCat, exposure, refCat, refWcs, exposureId=exposureId) 

690 

691 self.writeOutput(dataRef, forcedPhotResult.measCat) 

692 

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

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

695 

696 Parameters 

697 ---------- 

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

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

700 reference catalog. 

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

702 The measurement image upon which to perform forced detection. 

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

704 The reference catalog of sources to measure. 

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

706 The WCS for the references. 

707 exposureId : `int` 

708 Optional unique exposureId used for random seed in measurement 

709 task. 

710 

711 Returns 

712 ------- 

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

714 Structure with fields: 

715 

716 ``measCat`` 

717 Catalog of forced measurement results 

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

719 """ 

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

721 if self.config.doApCorr: 

722 self.applyApCorr.run( 

723 catalog=measCat, 

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

725 ) 

726 self.catalogCalculation.run(measCat) 

727 

728 return pipeBase.Struct(measCat=measCat) 

729 

730 def makeIdFactory(self, dataRef): 

731 """Create an object that generates globally unique source IDs. 

732 

733 Source IDs are created based on a per-CCD ID and the ID of the CCD 

734 itself. 

735 

736 Parameters 

737 ---------- 

738 dataRef : `lsst.daf.persistence.ButlerDataRef` 

739 Butler data reference. The ``ccdExposureId_bits`` and 

740 ``ccdExposureId`` datasets are accessed. The data ID must have the 

741 keys that correspond to ``ccdExposureId``, which are generally the 

742 same as those that correspond to ``calexp`` (``visit``, ``raft``, 

743 ``sensor`` for LSST data). 

744 """ 

745 exposureIdInfo = ExposureIdInfo(int(dataRef.get("ccdExposureId")), dataRef.get("ccdExposureId_bits")) 

746 return exposureIdInfo.makeSourceIdFactory() 

747 

748 def getExposureId(self, dataRef): 

749 return int(dataRef.get("ccdExposureId", immediate=True)) 

750 

751 def fetchReferences(self, dataRef, exposure): 

752 """Get sources that overlap the exposure. 

753 

754 Parameters 

755 ---------- 

756 dataRef : `lsst.daf.persistence.ButlerDataRef` 

757 Butler data reference corresponding to the image to be measured; 

758 should have ``tract``, ``patch``, and ``filter`` keys. 

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

760 The image to be measured (used only to obtain a WCS and bounding 

761 box). 

762 

763 Returns 

764 ------- 

765 referencs : `lsst.afw.table.SourceCatalog` 

766 Catalog of sources that overlap the exposure 

767 

768 Notes 

769 ----- 

770 The returned catalog is sorted by ID and guarantees that all included 

771 children have their parent included and that all Footprints are valid. 

772 

773 All work is delegated to the references subtask; see 

774 :lsst-task:`lsst.meas.base.references.CoaddSrcReferencesTask` 

775 for information about the default behavior. 

776 """ 

777 references = lsst.afw.table.SourceCatalog(self.references.schema) 

778 badParents = set() 

779 unfiltered = self.references.fetchInBox(dataRef, exposure.getBBox(), exposure.getWcs()) 

780 for record in unfiltered: 

781 if record.getFootprint() is None or record.getFootprint().getArea() == 0: 

782 if record.getParent() != 0: 

783 self.log.warning("Skipping reference %s (child of %s) with bad Footprint", 

784 record.getId(), record.getParent()) 

785 else: 

786 self.log.warning("Skipping reference parent %s with bad Footprint", record.getId()) 

787 badParents.add(record.getId()) 

788 elif record.getParent() not in badParents: 

789 references.append(record) 

790 # catalog must be sorted by parent ID for lsst.afw.table.getChildren to work 

791 references.sort(lsst.afw.table.SourceTable.getParentKey()) 

792 return references 

793 

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

795 r"""Attach footprints to blank sources prior to measurements. 

796 

797 Notes 

798 ----- 

799 `~lsst.afw.detection.Footprint`\ s for forced photometry must be in the 

800 pixel coordinate system of the image being measured, while the actual 

801 detections may start out in a different coordinate system. 

802 

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

804 those `~lsst.afw.detection.Footprint`\ s should be generated. 

805 

806 This default implementation transforms depends on the 

807 ``footprintSource`` configuration parameter. 

808 """ 

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

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

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

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

813 scaling=self.config.psfFootprintScaling) 

814 

815 def getExposure(self, dataRef): 

816 """Read input exposure for measurement. 

817 

818 Parameters 

819 ---------- 

820 dataRef : `lsst.daf.persistence.ButlerDataRef` 

821 Butler data reference. 

822 """ 

823 exposure = dataRef.get(self.dataPrefix + "calexp", immediate=True) 

824 

825 if self.config.doApplyExternalPhotoCalib: 

826 source = f"{self.config.externalPhotoCalibName}_photoCalib" 

827 self.log.info("Applying external photoCalib from %s", source) 

828 photoCalib = dataRef.get(source) 

829 exposure.setPhotoCalib(photoCalib) # No need for calibrateImage; having the photoCalib suffices 

830 

831 if self.config.doApplyExternalSkyWcs: 

832 source = f"{self.config.externalSkyWcsName}_wcs" 

833 self.log.info("Applying external skyWcs from %s", source) 

834 skyWcs = dataRef.get(source) 

835 exposure.setWcs(skyWcs) 

836 

837 if self.config.doApplySkyCorr: 

838 self.log.info("Apply sky correction") 

839 skyCorr = dataRef.get("skyCorr") 

840 exposure.maskedImage -= skyCorr.getImage() 

841 

842 return exposure 

843 

844 def writeOutput(self, dataRef, sources): 

845 """Write forced source table 

846 

847 Parameters 

848 ---------- 

849 dataRef : `lsst.daf.persistence.ButlerDataRef` 

850 Butler data reference. The forced_src dataset (with 

851 self.dataPrefix prepended) is all that will be modified. 

852 sources : `lsst.afw.table.SourceCatalog` 

853 Catalog of sources to save. 

854 """ 

855 dataRef.put(sources, self.dataPrefix + "forced_src", flags=lsst.afw.table.SOURCE_IO_NO_FOOTPRINTS) 

856 

857 def getSchemaCatalogs(self): 

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

859 

860 Returns 

861 ------- 

862 schemaCatalogs : `dict` 

863 Dictionary mapping dataset type to schema catalog. 

864 

865 Notes 

866 ----- 

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

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

869 """ 

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

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

872 datasetType = self.dataPrefix + "forced_src" 

873 return {datasetType: catalog} 

874 

875 def _getConfigName(self): 

876 # Documented in superclass. 

877 return self.dataPrefix + "forcedPhotCcd_config" 

878 

879 def _getMetadataName(self): 

880 # Documented in superclass 

881 return self.dataPrefix + "forcedPhotCcd_metadata" 

882 

883 @classmethod 

884 def _makeArgumentParser(cls): 

885 parser = pipeBase.ArgumentParser(name=cls._DefaultName) 

886 parser.add_id_argument("--id", "forced_src", help="data ID with raw CCD keys [+ tract optionally], " 

887 "e.g. --id visit=12345 ccd=1,2 [tract=0]", 

888 ContainerClass=PerTractCcdDataIdContainer) 

889 return parser 

890 

891 

892class ForcedPhotCcdFromDataFrameConnections(PipelineTaskConnections, 

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

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

895 "inputName": "calexp", 

896 "skyWcsName": "jointcal", 

897 "photoCalibName": "fgcm"}): 

898 refCat = cT.Input( 

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

900 name="{inputCoaddName}Diff_fullDiaObjTable", 

901 storageClass="DataFrame", 

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

903 multiple=True, 

904 deferLoad=True, 

905 ) 

906 exposure = cT.Input( 

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

908 name="{inputName}", 

909 storageClass="ExposureF", 

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

911 ) 

912 skyCorr = cT.Input( 

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

914 name="skyCorr", 

915 storageClass="Background", 

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

917 ) 

918 externalSkyWcsTractCatalog = cT.Input( 

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

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

921 name="{skyWcsName}SkyWcsCatalog", 

922 storageClass="ExposureCatalog", 

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

924 ) 

925 externalSkyWcsGlobalCatalog = cT.Input( 

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

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

928 "fast lookup."), 

929 name="{skyWcsName}SkyWcsCatalog", 

930 storageClass="ExposureCatalog", 

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

932 ) 

933 externalPhotoCalibTractCatalog = cT.Input( 

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

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

936 name="{photoCalibName}PhotoCalibCatalog", 

937 storageClass="ExposureCatalog", 

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

939 ) 

940 externalPhotoCalibGlobalCatalog = cT.Input( 

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

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

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

944 name="{photoCalibName}PhotoCalibCatalog", 

945 storageClass="ExposureCatalog", 

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

947 ) 

948 finalizedPsfApCorrCatalog = cT.Input( 

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

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

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

952 name="finalized_psf_ap_corr_catalog", 

953 storageClass="ExposureCatalog", 

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

955 ) 

956 measCat = cT.Output( 

957 doc="Output forced photometry catalog.", 

958 name="forced_src_diaObject", 

959 storageClass="SourceCatalog", 

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

961 ) 

962 outputSchema = cT.InitOutput( 

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

964 name="forced_src_diaObject_schema", 

965 storageClass="SourceCatalog", 

966 ) 

967 

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

969 super().__init__(config=config) 

970 if not config.doApplySkyCorr: 

971 self.inputs.remove("skyCorr") 

972 if config.doApplyExternalSkyWcs: 

973 if config.useGlobalExternalSkyWcs: 

974 self.inputs.remove("externalSkyWcsTractCatalog") 

975 else: 

976 self.inputs.remove("externalSkyWcsGlobalCatalog") 

977 else: 

978 self.inputs.remove("externalSkyWcsTractCatalog") 

979 self.inputs.remove("externalSkyWcsGlobalCatalog") 

980 if config.doApplyExternalPhotoCalib: 

981 if config.useGlobalExternalPhotoCalib: 

982 self.inputs.remove("externalPhotoCalibTractCatalog") 

983 else: 

984 self.inputs.remove("externalPhotoCalibGlobalCatalog") 

985 else: 

986 self.inputs.remove("externalPhotoCalibTractCatalog") 

987 self.inputs.remove("externalPhotoCalibGlobalCatalog") 

988 if not config.doApplyFinalizedPsf: 

989 self.inputs.remove("finalizedPsfApCorrCatalog") 

990 

991 

992class ForcedPhotCcdFromDataFrameConfig(ForcedPhotCcdConfig, 

993 pipelineConnections=ForcedPhotCcdFromDataFrameConnections): 

994 def setDefaults(self): 

995 super().setDefaults() 

996 self.footprintSource = "psf" 

997 self.measurement.doReplaceWithNoise = False 

998 self.measurement.plugins = ["base_TransformedCentroidFromCoord", "base_PsfFlux", "base_PixelFlags"] 

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

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

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

1002 self.measurement.slots.shape = None 

1003 

1004 def validate(self): 

1005 super().validate() 

1006 if self.footprintSource == "transformed": 

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

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

1009 

1010 

1011class ForcedPhotCcdFromDataFrameTask(ForcedPhotCcdTask): 

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

1013 

1014 Uses input from a DataFrame instead of SourceCatalog 

1015 like the base class ForcedPhotCcd does. 

1016 Writes out a SourceCatalog so that the downstream 

1017 WriteForcedSourceTableTask can be reused with output from this Task. 

1018 """ 

1019 _DefaultName = "forcedPhotCcdFromDataFrame" 

1020 ConfigClass = ForcedPhotCcdFromDataFrameConfig 

1021 

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

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

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

1025 

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

1027 

1028 if self.config.doApCorr: 

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

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

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

1032 

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

1034 inputs = butlerQC.get(inputRefs) 

1035 

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

1037 inputs['refWcs'] = None 

1038 

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

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

1041 if self.config.useGlobalExternalSkyWcs: 

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

1043 else: 

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

1045 if self.config.useGlobalExternalPhotoCalib: 

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

1047 else: 

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

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

1050 

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

1052 inputs['exposure'], 

1053 skyCorr=skyCorr, 

1054 externalSkyWcsCatalog=externalSkyWcsCatalog, 

1055 externalPhotoCalibCatalog=externalPhotoCalibCatalog, 

1056 finalizedPsfApCorrCatalog=finalizedPsfApCorrCatalog 

1057 ) 

1058 

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

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

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

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

1063 inputs['refCat'] = refCat 

1064 # generateMeasCat does not use the refWcs. 

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

1066 inputs['exposure'], inputs['refCat'], 

1067 inputs['refWcs'], 

1068 "visit_detector") 

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

1070 # supported in the DataFrame-backed task. 

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

1072 outputs = self.run(**inputs) 

1073 

1074 butlerQC.put(outputs, outputRefs) 

1075 

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

1077 """Convert list of DataFrames to reference catalog 

1078 

1079 Concatenate list of DataFrames presumably from multiple patches and 

1080 downselect rows that overlap the exposureBBox using the exposureWcs. 

1081 

1082 Parameters 

1083 ---------- 

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

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

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

1087 exposureBBox : `lsst.geom.Box2I` 

1088 Bounding box on which to select rows that overlap 

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

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

1091 pixel coords with which to compare with exposureBBox 

1092 

1093 Returns 

1094 ------- 

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

1096 Source Catalog with minimal schema that overlaps exposureBBox 

1097 """ 

1098 df = pd.concat(dfList) 

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

1100 # to down select rows that overlap the detector bbox 

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

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

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

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

1105 return refCat 

1106 

1107 def df2SourceCat(self, df): 

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

1109 

1110 The forced measurement subtask expects this as input. 

1111 

1112 Parameters 

1113 ---------- 

1114 df : `pandas.DataFrame` 

1115 DiaObjects with locations and ids. 

1116 

1117 Returns 

1118 ------- 

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

1120 Output catalog with minimal schema. 

1121 """ 

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

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

1124 outputCatalog.reserve(len(df)) 

1125 

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

1127 outputRecord = outputCatalog.addNew() 

1128 outputRecord.setId(diaObjectId) 

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

1130 return outputCatalog