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

225 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-25 08:24 +0000

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 dataclasses 

23from deprecated.sphinx import deprecated 

24 

25import astropy.table 

26import pandas as pd 

27import numpy as np 

28 

29import lsst.pex.config 

30import lsst.pex.exceptions 

31import lsst.pipe.base 

32import lsst.geom 

33import lsst.afw.detection 

34import lsst.afw.geom 

35import lsst.afw.image 

36import lsst.afw.table 

37import lsst.sphgeom 

38 

39from lsst.pipe.base import PipelineTaskConnections, NoWorkFound 

40import lsst.pipe.base.connectionTypes as cT 

41 

42import lsst.pipe.base as pipeBase 

43from lsst.skymap import BaseSkyMap 

44 

45from .forcedMeasurement import ForcedMeasurementTask 

46from .applyApCorr import ApplyApCorrTask 

47from .catalogCalculation import CatalogCalculationTask 

48from ._id_generator import DetectorVisitIdGeneratorConfig 

49 

50__all__ = ("ForcedPhotCcdConfig", "ForcedPhotCcdTask", 

51 "ForcedPhotCcdFromDataFrameTask", "ForcedPhotCcdFromDataFrameConfig") 

52 

53 

54@deprecated(reason="This task is replaced by lsst.pipe.tasks.ForcedPhotCcdTask. " 

55 "This task will be removed after v30.", 

56 version="v29.0", category=FutureWarning) 

57class ForcedPhotCcdConnections(PipelineTaskConnections, 

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

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

60 "inputName": "calexp"}): 

61 inputSchema = cT.InitInput( 

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

63 name="{inputCoaddName}Coadd_ref_schema", 

64 storageClass="SourceCatalog", 

65 ) 

66 outputSchema = cT.InitOutput( 

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

68 name="forced_src_schema", 

69 storageClass="SourceCatalog", 

70 ) 

71 exposure = cT.Input( 

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

73 name="{inputName}", 

74 storageClass="ExposureF", 

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

76 ) 

77 refCat = cT.Input( 

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

79 name="{inputCoaddName}Coadd_ref", 

80 storageClass="SourceCatalog", 

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

82 multiple=True, 

83 deferLoad=True, 

84 ) 

85 skyMap = cT.Input( 

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

87 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME, 

88 storageClass="SkyMap", 

89 dimensions=["skymap"], 

90 ) 

91 skyCorr = cT.Input( 

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

93 name="skyCorr", 

94 storageClass="Background", 

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

96 ) 

97 visitSummary = cT.Input( 

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

99 name="finalVisitSummary", 

100 storageClass="ExposureCatalog", 

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

102 ) 

103 measCat = cT.Output( 

104 doc="Output forced photometry catalog.", 

105 name="forced_src", 

106 storageClass="SourceCatalog", 

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

108 ) 

109 

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

111 super().__init__(config=config) 

112 if not config.doApplySkyCorr: 

113 del self.skyCorr 

114 if not config.useVisitSummary: 

115 del self.visitSummary 

116 if config.refCatStorageClass != "SourceCatalog": 

117 del self.inputSchema 

118 # Connections are immutable, so we have to replace them entirely 

119 # rather than edit them in-place. 

120 self.refCat = dataclasses.replace(self.refCat, storageClass=config.refCatStorageClass) 

121 

122 

123@deprecated(reason="This task is replaced by lsst.pipe.tasks.ForcedPhotCcdTask. " 

124 "This task will be removed after v30.", 

125 version="v29.0", category=FutureWarning) 

126class ForcedPhotCcdConfig(pipeBase.PipelineTaskConfig, 

127 pipelineConnections=ForcedPhotCcdConnections): 

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

129 measurement = lsst.pex.config.ConfigurableField( 

130 target=ForcedMeasurementTask, 

131 doc="subtask to do forced measurement" 

132 ) 

133 coaddName = lsst.pex.config.Field( 

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

135 dtype=str, 

136 default="deep", 

137 ) 

138 doApCorr = lsst.pex.config.Field( 

139 dtype=bool, 

140 default=True, 

141 doc="Run subtask to apply aperture corrections" 

142 ) 

143 applyApCorr = lsst.pex.config.ConfigurableField( 

144 target=ApplyApCorrTask, 

145 doc="Subtask to apply aperture corrections" 

146 ) 

147 catalogCalculation = lsst.pex.config.ConfigurableField( 

148 target=CatalogCalculationTask, 

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

150 ) 

151 doApplySkyCorr = lsst.pex.config.Field( 

152 dtype=bool, 

153 default=False, 

154 doc="Apply sky correction?", 

155 ) 

156 useVisitSummary = lsst.pex.config.Field( 

157 dtype=bool, 

158 default=True, 

159 doc=( 

160 "Use updated WCS, PhotoCalib, ApCorr, and PSF from visit summary? " 

161 "This should be False if and only if the input image already has the best-available calibration " 

162 "objects attached." 

163 ), 

164 ) 

165 refCatStorageClass = lsst.pex.config.ChoiceField( 

166 dtype=str, 

167 allowed={ 

168 "SourceCatalog": "Read an lsst.afw.table.SourceCatalog.", 

169 "DataFrame": "Read a pandas.DataFrame.", 

170 "ArrowAstropy": "Read an astropy.table.Table saved to Parquet.", 

171 }, 

172 default="SourceCatalog", 

173 doc=( 

174 "The butler storage class for the refCat connection. " 

175 "If set to something other than 'SourceCatalog', the " 

176 "'inputSchema' connection will be ignored." 

177 ) 

178 ) 

179 refCatIdColumn = lsst.pex.config.Field( 

180 dtype=str, 

181 default="diaObjectId", 

182 doc=( 

183 "Name of the column that provides the object ID from the refCat connection. " 

184 "measurement.copyColumns['id'] must be set to this value as well." 

185 "Ignored if refCatStorageClass='SourceCatalog'." 

186 ) 

187 ) 

188 refCatRaColumn = lsst.pex.config.Field( 

189 dtype=str, 

190 default="ra", 

191 doc=( 

192 "Name of the column that provides the right ascension (in floating-point degrees) from the " 

193 "refCat connection. " 

194 "Ignored if refCatStorageClass='SourceCatalog'." 

195 ) 

196 ) 

197 refCatDecColumn = lsst.pex.config.Field( 

198 dtype=str, 

199 default="dec", 

200 doc=( 

201 "Name of the column that provides the declination (in floating-point degrees) from the " 

202 "refCat connection. " 

203 "Ignored if refCatStorageClass='SourceCatalog'." 

204 ) 

205 ) 

206 # TODO[DM-49400]: remove this config option; it already does nothing. 

207 includePhotoCalibVar = lsst.pex.config.Field( 

208 dtype=bool, 

209 default=False, 

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

211 deprecated="Deprecated and unused; will be removed after v29.", 

212 ) 

213 footprintSource = lsst.pex.config.ChoiceField( 

214 dtype=str, 

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

216 allowed={ 

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

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

219 "HeavyFootprints)."), 

220 }, 

221 optional=True, 

222 default="transformed", 

223 ) 

224 psfFootprintScaling = lsst.pex.config.Field( 

225 dtype=float, 

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

227 default=3.0, 

228 ) 

229 idGenerator = DetectorVisitIdGeneratorConfig.make_field() 

230 

231 def setDefaults(self): 

232 # Docstring inherited. 

233 super().setDefaults() 

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

235 # a biased correction for blended neighbors. 

236 self.measurement.doReplaceWithNoise = False 

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

238 # needed for PSF-like sources. 

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

240 "base_TransformedCentroid", 

241 "base_PsfFlux", 

242 "base_LocalBackground", 

243 "base_LocalPhotoCalib", 

244 "base_LocalWcs", 

245 ] 

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

247 self.measurement.slots.shape = None 

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

249 # by default in ForcedMeasurementTask. 

250 self.catalogCalculation.plugins.names = [] 

251 

252 def validate(self): 

253 super().validate() 

254 if self.refCatStorageClass != "SourceCatalog": 

255 if self.footprintSource == "transformed": 

256 raise ValueError("Cannot transform footprints from reference catalog, because " 

257 f"{self.config.refCatStorageClass} datasets can't hold footprints.") 

258 if self.measurement.copyColumns["id"] != self.refCatIdColumn: 

259 raise ValueError( 

260 f"measurement.copyColumns['id'] should be set to {self.refCatIdColumn} " 

261 f"(refCatIdColumn) when refCatStorageClass={self.refCatStorageClass}." 

262 ) 

263 

264 def configureParquetRefCat(self, refCatStorageClass: str = "ArrowAstropy"): 

265 """Set the refCatStorageClass option to a Parquet-based type, and 

266 reconfigure the measurement subtask and footprintSources accordingly. 

267 """ 

268 self.refCatStorageClass = refCatStorageClass 

269 self.footprintSource = "psf" 

270 self.measurement.doReplaceWithNoise = False 

271 self.measurement.plugins.names -= {"base_TransformedCentroid"} 

272 self.measurement.plugins.names |= {"base_TransformedCentroidFromCoord"} 

273 self.measurement.copyColumns["id"] = self.refCatIdColumn 

274 self.measurement.copyColumns.pop("deblend_nChild", None) 

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

276 

277 

278@deprecated(reason="This task is replaced by lsst.pipe.tasks.ForcedPhotCcdTask. " 

279 "This task will be removed after v30.", 

280 version="v29.0", category=FutureWarning) 

281class ForcedPhotCcdTask(pipeBase.PipelineTask): 

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

283 

284 Parameters 

285 ---------- 

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

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

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

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

290 initInputs : `dict` 

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

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

293 **kwargs 

294 Keyword arguments are passed to the supertask constructor. 

295 """ 

296 

297 ConfigClass = ForcedPhotCcdConfig 

298 _DefaultName = "forcedPhotCcd" 

299 dataPrefix = "" 

300 

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

302 super().__init__(**kwargs) 

303 

304 if initInputs: 

305 refSchema = initInputs['inputSchema'].schema 

306 

307 if refSchema is None: 

308 refSchema = lsst.afw.table.SourceTable.makeMinimalSchema() 

309 

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

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

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

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

314 if self.config.doApCorr: 

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

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

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

318 

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

320 inputs = butlerQC.get(inputRefs) 

321 

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

323 skyMap = inputs.pop('skyMap') 

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

325 

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

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

328 

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

330 inputs['exposure'], 

331 skyCorr=skyCorr, 

332 visitSummary=inputs.pop("visitSummary", None), 

333 ) 

334 

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

336 raise NoWorkFound("Exposure has no WCS.") 

337 

338 match self.config.refCatStorageClass: 

339 case "SourceCatalog": 

340 prepFunc = self._prepSourceCatalogRefCat 

341 case "DataFrame": 

342 prepFunc = self._prepDataFrameRefCat 

343 case "ArrowAstropy": 

344 prepFunc = self._prepArrowAstropyRefCat 

345 case _: 

346 raise AssertionError("Configuration should not have passed validation.") 

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

348 inputs['refCat'] = prepFunc( 

349 inputs['refCat'], 

350 inputs['exposure'].getBBox(), 

351 inputs['exposure'].getWcs(), 

352 ) 

353 # generateMeasCat does not actually use the refWcs; parameter is 

354 # passed for signature backwards compatibility. 

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

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

357 ) 

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

359 # not supported unless refCatStorageClass='SourceCatalog'. 

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

361 outputs = self.run(**inputs) 

362 butlerQC.put(outputs, outputRefs) 

363 

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

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

366 and sky corrections if so configured. 

367 

368 Parameters 

369 ---------- 

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

371 Input exposure to adjust calibrations. 

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

373 Sky correction frame to apply if doApplySkyCorr=True. 

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

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

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

377 overridden by other arguments. 

378 

379 Returns 

380 ------- 

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

382 Exposure with adjusted calibrations. 

383 """ 

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

385 

386 if visitSummary is not None: 

387 row = visitSummary.find(detectorId) 

388 if row is None: 

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

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

391 exposure.setPhotoCalib(photoCalib) 

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

393 exposure.setWcs(skyWcs) 

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

395 exposure.setPsf(psf) 

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

397 exposure.info.setApCorrMap(apCorrMap) 

398 

399 if skyCorr is not None: 

400 exposure.maskedImage -= skyCorr.getImage() 

401 

402 return exposure 

403 

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

405 """Generate a measurement catalog. 

406 

407 Parameters 

408 ---------- 

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

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

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

412 Exposure to generate the catalog for. 

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

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

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

416 Reference world coordinate system. 

417 This parameter is not currently used. 

418 

419 Returns 

420 ------- 

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

422 Catalog of forced sources to measure. 

423 expId : `int` 

424 Unique binary id associated with the input exposure 

425 """ 

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

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

428 idFactory=id_generator.make_table_id_factory()) 

429 return measCat, id_generator.catalog_id 

430 

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

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

433 

434 Parameters 

435 ---------- 

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

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

438 reference catalog. 

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

440 The measurement image upon which to perform forced detection. 

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

442 The reference catalog of sources to measure. 

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

444 The WCS for the references. 

445 exposureId : `int` 

446 Optional unique exposureId used for random seed in measurement 

447 task. 

448 

449 Returns 

450 ------- 

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

452 Structure with fields: 

453 

454 ``measCat`` 

455 Catalog of forced measurement results 

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

457 """ 

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

459 if self.config.doApCorr: 

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

461 if apCorrMap is None: 

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

463 else: 

464 self.applyApCorr.run( 

465 catalog=measCat, 

466 apCorrMap=apCorrMap, 

467 ) 

468 self.catalogCalculation.run(measCat) 

469 

470 return pipeBase.Struct(measCat=measCat) 

471 

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

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

474 

475 Notes 

476 ----- 

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

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

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

480 

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

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

483 

484 This default implementation transforms depends on the 

485 ``footprintSource`` configuration parameter. 

486 """ 

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

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

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

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

491 scaling=self.config.psfFootprintScaling) 

492 

493 def _prepSourceCatalogRefCat(self, refCatHandles, exposureBBox, exposureWcs): 

494 """Prepare a merged, filtered reference catalog from SourceCatalog 

495 inputs. 

496 

497 Parameters 

498 ---------- 

499 refCatHandles : sequence of `lsst.daf.butler.DeferredDatasetHandle` 

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

501 photometry. 

502 exposureBBox : `lsst.geom.Box2I` 

503 Bounding box on which to select rows that overlap 

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

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

506 pixel coords with which to compare with exposureBBox 

507 

508 Returns 

509 ------- 

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

511 Filtered catalog of forced sources to measure. 

512 

513 Notes 

514 ----- 

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

516 lsst.meas.algorithms.loadReferenceObjects.ReferenceObjectLoader 

517 

518 """ 

519 mergedRefCat = None 

520 

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

522 # be performed on. 

523 expBBox = lsst.geom.Box2D(exposureBBox) 

524 expBoxCorners = expBBox.getCorners() 

525 expSkyCorners = [exposureWcs.pixelToSky(corner).getVector() for corner in expBoxCorners] 

526 expPolygon = lsst.sphgeom.ConvexPolygon(expSkyCorners) 

527 

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

529 # not contained within the exposure boundaries, or whose 

530 # parents are not within the exposure boundaries. Note 

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

532 # appear before the children. 

533 for refCat in refCatHandles: 

534 refCat = refCat.get() 

535 if mergedRefCat is None: 

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

537 # zero as a parent ID means "this is a parent" 

538 containedIds = np.array([0]) 

539 

540 coordKey = refCat.getCoordKey() 

541 inside = expPolygon.contains(lon=refCat[coordKey.getRa()], lat=refCat[coordKey.getDec()]) 

542 parentIds = refCat[refCat.getParentKey()] 

543 inside &= np.isin(parentIds, containedIds) 

544 

545 mergedRefCat.extend(refCat[inside]) 

546 containedIds = np.union1d(containedIds, refCat[refCat.getIdKey()][inside]) 

547 

548 if mergedRefCat is None: 

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

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

551 return mergedRefCat 

552 

553 def _prepDataFrameRefCat(self, refCatHandles, exposureBBox, exposureWcs): 

554 """Prepare a merged, filtered reference catalog from DataFrame 

555 inputs. 

556 

557 Parameters 

558 ---------- 

559 refCatHandles : sequence of `lsst.daf.butler.DeferredDatasetHandle` 

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

561 photometry. 

562 exposureBBox : `lsst.geom.Box2I` 

563 Bounding box on which to select rows that overlap 

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

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

566 pixel coords with which to compare with exposureBBox 

567 

568 Returns 

569 ------- 

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

571 Source Catalog with minimal schema that overlaps exposureBBox 

572 """ 

573 dfList = [ 

574 i.get( 

575 parameters={ 

576 "columns": [ 

577 self.config.refCatIdColumn, 

578 self.config.refCatRaColumn, 

579 self.config.refCatDecColumn, 

580 ] 

581 } 

582 ) 

583 for i in refCatHandles 

584 ] 

585 df = pd.concat(dfList) 

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

587 # to down select rows that overlap the detector bbox 

588 x, y = exposureWcs.skyToPixelArray( 

589 df[self.config.refCatRaColumn].values, 

590 df[self.config.refCatDecColumn].values, 

591 degrees=True, 

592 ) 

593 inBBox = np.atleast_1d(lsst.geom.Box2D(exposureBBox).contains(x, y)) 

594 refCat = self._makeMinimalSourceCatalogFromDataFrame(df[inBBox]) 

595 return refCat 

596 

597 def _prepArrowAstropyRefCat(self, refCatHandles, exposureBBox, exposureWcs): 

598 """Prepare a merged, filtered reference catalog from ArrowAstropy 

599 inputs. 

600 

601 Parameters 

602 ---------- 

603 refCatHandles : sequence of `lsst.daf.butler.DeferredDatasetHandle` 

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

605 photometry. 

606 exposureBBox : `lsst.geom.Box2I` 

607 Bounding box on which to select rows that overlap 

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

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

610 pixel coords with which to compare with exposureBBox 

611 

612 Returns 

613 ------- 

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

615 Source Catalog with minimal schema that overlaps exposureBBox 

616 """ 

617 table_list = [ 

618 i.get( 

619 parameters={ 

620 "columns": [ 

621 self.config.refCatIdColumn, 

622 self.config.refCatRaColumn, 

623 self.config.refCatDecColumn, 

624 ] 

625 } 

626 ) 

627 for i in refCatHandles 

628 ] 

629 full_table = astropy.table.vstack(table_list) 

630 # translate ra/dec coords in table to detector pixel coords 

631 # to down-select rows that overlap the detector bbox 

632 x, y = exposureWcs.skyToPixelArray( 

633 full_table[self.config.refCatRaColumn], 

634 full_table[self.config.refCatDecColumn], 

635 degrees=True, 

636 ) 

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

638 refCat = self._makeMinimalSourceCatalogFromAstropy(full_table[inBBox]) 

639 return refCat 

640 

641 def _makeMinimalSourceCatalogFromDataFrame(self, df): 

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

643 

644 The forced measurement subtask expects this as input. 

645 

646 Parameters 

647 ---------- 

648 df : `pandas.DataFrame` 

649 Table with locations and ids. 

650 

651 Returns 

652 ------- 

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

654 Output catalog with minimal schema. 

655 """ 

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

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

658 outputCatalog.reserve(len(df)) 

659 

660 for objectId, ra, dec in df[['ra', 'dec']].itertuples(): 

661 outputRecord = outputCatalog.addNew() 

662 outputRecord.setId(objectId) 

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

664 return outputCatalog 

665 

666 def _makeMinimalSourceCatalogFromAstropy(self, table): 

667 """Create minimal schema SourceCatalog from an Astropy Table. 

668 

669 The forced measurement subtask expects this as input. 

670 

671 Parameters 

672 ---------- 

673 table : `astropy.table.Table` 

674 Table with locations and ids. 

675 

676 Returns 

677 ------- 

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

679 Output catalog with minimal schema. 

680 """ 

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

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

683 outputCatalog.reserve(len(table)) 

684 

685 for objectId, ra, dec in table.iterrows(): 

686 outputRecord = outputCatalog.addNew() 

687 outputRecord.setId(objectId) 

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

689 return outputCatalog 

690 

691 

692@deprecated(reason="This task is replaced by lsst.pipe.tasks.ForcedPhotCcdTask. " 

693 "This task will be removed after v30.", 

694 version="v29.0", category=FutureWarning) 

695class ForcedPhotCcdFromDataFrameConnections(ForcedPhotCcdConnections, 

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

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

698 "inputName": "calexp", 

699 }): 

700 pass 

701 

702 

703@deprecated(reason="This task is replaced by lsst.pipe.tasks.ForcedPhotCcdTask. " 

704 "This task will be removed after v30.", 

705 version="v29.0", category=FutureWarning) 

706class ForcedPhotCcdFromDataFrameConfig(ForcedPhotCcdConfig, 

707 pipelineConnections=ForcedPhotCcdFromDataFrameConnections): 

708 def setDefaults(self): 

709 super().setDefaults() 

710 self.configureParquetRefCat("DataFrame") 

711 self.connections.refCat = "{inputCoaddName}Diff_fullDiaObjTable" 

712 self.connections.outputSchema = "forced_src_diaObject_schema" 

713 self.connections.measCat = "forced_src_diaObject" 

714 

715 

716@deprecated(reason="This task is replaced by lsst.pipe.tasks.ForcedPhotCcdTask. " 

717 "This task will be removed after v30.", 

718 version="v29.0", category=FutureWarning) 

719class ForcedPhotCcdFromDataFrameTask(ForcedPhotCcdTask): 

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

721 

722 Uses input from a DataFrame instead of SourceCatalog 

723 like the base class ForcedPhotCcd does. 

724 Writes out a SourceCatalog so that the downstream 

725 WriteForcedSourceTableTask can be reused with output from this Task. 

726 """ 

727 _DefaultName = "forcedPhotCcdFromDataFrame" 

728 ConfigClass = ForcedPhotCcdFromDataFrameConfig