Coverage for python / lsst / pipe / tasks / processCcdWithFakes.py: 0%

235 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-17 09:21 +0000

1# This file is part of pipe_tasks. 

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 

22""" 

23Insert fake sources into calexps 

24""" 

25 

26__all__ = ["ProcessCcdWithFakesConfig", "ProcessCcdWithFakesTask", 

27 "ProcessCcdWithVariableFakesConfig", "ProcessCcdWithVariableFakesTask"] 

28 

29import numpy as np 

30import pandas as pd 

31 

32import lsst.pex.config as pexConfig 

33import lsst.pipe.base as pipeBase 

34 

35from .insertFakes import InsertFakesTask 

36from lsst.afw.table import SourceTable 

37from lsst.meas.base import IdGenerator, DetectorVisitIdGeneratorConfig 

38from lsst.pipe.base import PipelineTask, PipelineTaskConfig, PipelineTaskConnections 

39import lsst.pipe.base.connectionTypes as cT 

40import lsst.afw.table as afwTable 

41from lsst.skymap import BaseSkyMap 

42from lsst.pipe.tasks.calibrate import CalibrateTask 

43 

44from deprecated.sphinx import deprecated 

45 

46 

47class ProcessCcdWithFakesConnections(PipelineTaskConnections, 

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

49 defaultTemplates={"coaddName": "deep", 

50 "wcsName": "gbdesAstrometricFit", 

51 "photoCalibName": "jointcal", 

52 "fakesType": "fakes_"}): 

53 skyMap = cT.Input( 

54 doc="Input definition of geometry/bbox and projection/wcs for " 

55 "template exposures. Needed to test which tract to generate ", 

56 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME, 

57 dimensions=("skymap",), 

58 storageClass="SkyMap", 

59 ) 

60 

61 exposure = cT.Input( 

62 doc="Exposure into which fakes are to be added.", 

63 name="calexp", 

64 storageClass="ExposureF", 

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

66 ) 

67 

68 fakeCats = cT.Input( 

69 doc="Set of catalogs of fake sources to draw inputs from. We " 

70 "concatenate the tract catalogs for detectorVisits that cover " 

71 "multiple tracts.", 

72 name="{fakesType}fakeSourceCat", 

73 storageClass="DataFrame", 

74 dimensions=("tract", "skymap"), 

75 deferLoad=True, 

76 multiple=True, 

77 ) 

78 

79 externalSkyWcsTractCatalog = cT.Input( 

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

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

82 name="{wcsName}SkyWcsCatalog", 

83 storageClass="ExposureCatalog", 

84 dimensions=("instrument", "visit", "tract", "skymap"), 

85 deferLoad=True, 

86 multiple=True, 

87 ) 

88 

89 externalSkyWcsGlobalCatalog = cT.Input( 

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

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

92 "fast lookup."), 

93 name="finalVisitSummary", 

94 storageClass="ExposureCatalog", 

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

96 ) 

97 

98 externalPhotoCalibTractCatalog = cT.Input( 

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

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

101 name="{photoCalibName}PhotoCalibCatalog", 

102 storageClass="ExposureCatalog", 

103 dimensions=("instrument", "visit", "tract"), 

104 deferLoad=True, 

105 multiple=True, 

106 ) 

107 

108 externalPhotoCalibGlobalCatalog = cT.Input( 

109 doc=("Per-visit photometric calibrations. These catalogs use the " 

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

111 name="finalVisitSummary", 

112 storageClass="ExposureCatalog", 

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

114 ) 

115 

116 icSourceCat = cT.Input( 

117 doc="Catalog of calibration sources", 

118 name="icSrc", 

119 storageClass="SourceCatalog", 

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

121 ) 

122 

123 sfdSourceCat = cT.Input( 

124 doc="Catalog of calibration sources", 

125 name="src", 

126 storageClass="SourceCatalog", 

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

128 ) 

129 

130 outputExposure = cT.Output( 

131 doc="Exposure with fake sources added.", 

132 name="{fakesType}calexp", 

133 storageClass="ExposureF", 

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

135 ) 

136 

137 outputCat = cT.Output( 

138 doc="Source catalog produced in calibrate task with fakes also measured.", 

139 name="{fakesType}src", 

140 storageClass="SourceCatalog", 

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

142 ) 

143 

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

145 super().__init__(config=config) 

146 

147 if not config.doApplyExternalGlobalPhotoCalib: 

148 self.inputs.remove("externalPhotoCalibGlobalCatalog") 

149 if not config.doApplyExternalTractPhotoCalib: 

150 self.inputs.remove("externalPhotoCalibTractCatalog") 

151 

152 if not config.doApplyExternalGlobalSkyWcs: 

153 self.inputs.remove("externalSkyWcsGlobalCatalog") 

154 if not config.doApplyExternalTractSkyWcs: 

155 self.inputs.remove("externalSkyWcsTractCatalog") 

156 

157 

158@deprecated( 

159 reason="This task will be removed in v28.0 as it is replaced by `source_injection` tasks.", 

160 version="v28.0", 

161 category=FutureWarning, 

162) 

163class ProcessCcdWithFakesConfig(PipelineTaskConfig, 

164 pipelineConnections=ProcessCcdWithFakesConnections): 

165 """Config for inserting fake sources 

166 

167 Notes 

168 ----- 

169 The default column names are those from the UW sims database. 

170 """ 

171 

172 doApplyExternalGlobalPhotoCalib = pexConfig.Field( 

173 dtype=bool, 

174 default=False, 

175 doc="Whether to apply an external photometric calibration via an " 

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

177 "`externalPhotoCalibName` config option to determine which " 

178 "calibration to use. Uses a global calibration." 

179 ) 

180 

181 doApplyExternalTractPhotoCalib = pexConfig.Field( 

182 dtype=bool, 

183 default=False, 

184 doc="Whether to apply an external photometric calibration via an " 

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

186 "`externalPhotoCalibName` config option to determine which " 

187 "calibration to use. Uses a per tract calibration." 

188 ) 

189 

190 externalPhotoCalibName = pexConfig.ChoiceField( 

191 doc="What type of external photo calib to use.", 

192 dtype=str, 

193 default="jointcal", 

194 allowed={"jointcal": "Use jointcal_photoCalib", 

195 "fgcm": "Use fgcm_photoCalib", 

196 "fgcm_tract": "Use fgcm_tract_photoCalib"} 

197 ) 

198 

199 doApplyExternalGlobalSkyWcs = pexConfig.Field( 

200 dtype=bool, 

201 default=False, 

202 doc="Whether to apply an external astrometric calibration via an " 

203 "`lsst.afw.geom.SkyWcs` object. Uses the " 

204 "`externalSkyWcsName` config option to determine which " 

205 "calibration to use. Uses a global calibration." 

206 ) 

207 

208 doApplyExternalTractSkyWcs = pexConfig.Field( 

209 dtype=bool, 

210 default=False, 

211 doc="Whether to apply an external astrometric calibration via an " 

212 "`lsst.afw.geom.SkyWcs` object. Uses the " 

213 "`externalSkyWcsName` config option to determine which " 

214 "calibration to use. Uses a per tract calibration." 

215 ) 

216 

217 externalSkyWcsName = pexConfig.ChoiceField( 

218 doc="What type of updated WCS calib to use.", 

219 dtype=str, 

220 default="gbdesAstrometricFit", 

221 allowed={"gbdesAstrometricFit": "Use gbdesAstrometricFit_wcs"} 

222 ) 

223 

224 coaddName = pexConfig.Field( 

225 doc="The name of the type of coadd used", 

226 dtype=str, 

227 default="deep", 

228 ) 

229 

230 srcFieldsToCopy = pexConfig.ListField( 

231 dtype=str, 

232 default=("calib_photometry_reserved", "calib_photometry_used", "calib_astrometry_used", 

233 "calib_psf_candidate", "calib_psf_used", "calib_psf_reserved"), 

234 doc=("Fields to copy from the `src` catalog to the output catalog " 

235 "for matching sources Any missing fields will trigger a " 

236 "RuntimeError exception.") 

237 ) 

238 

239 matchRadiusPix = pexConfig.Field( 

240 dtype=float, 

241 default=3, 

242 doc=("Match radius for matching icSourceCat objects to sourceCat objects (pixels)"), 

243 ) 

244 

245 doMatchVisit = pexConfig.Field( 

246 dtype=bool, 

247 default=False, 

248 doc="Match visit to trim the fakeCat" 

249 ) 

250 

251 calibrate = pexConfig.ConfigurableField(target=CalibrateTask, 

252 doc="The calibration task to use.") 

253 

254 insertFakes = pexConfig.ConfigurableField(target=InsertFakesTask, 

255 doc="Configuration for the fake sources") 

256 

257 idGenerator = DetectorVisitIdGeneratorConfig.make_field() 

258 

259 def setDefaults(self): 

260 super().setDefaults() 

261 self.calibrate.measurement.plugins["base_PixelFlags"].masksFpAnywhere.append("FAKE") 

262 self.calibrate.measurement.plugins["base_PixelFlags"].masksFpCenter.append("FAKE") 

263 self.calibrate.doAstrometry = False 

264 self.calibrate.doWriteMatches = False 

265 self.calibrate.doPhotoCal = False 

266 self.calibrate.doComputeSummaryStats = False 

267 self.calibrate.detection.reEstimateBackground = False 

268 

269 

270@deprecated( 

271 reason="This task will be removed in v28.0 as it is replaced by `source_injection` tasks.", 

272 version="v28.0", 

273 category=FutureWarning, 

274) 

275class ProcessCcdWithFakesTask(PipelineTask): 

276 """Insert fake objects into calexps. 

277 

278 Add fake stars and galaxies to the given calexp, specified in the dataRef. Galaxy parameters are read in 

279 from the specified file and then modelled using galsim. Re-runs characterize image and calibrate image to 

280 give a new background estimation and measurement of the calexp. 

281 

282 `ProcessFakeSourcesTask` inherits six functions from insertFakesTask that make images of the fake 

283 sources and then add them to the calexp. 

284 

285 `addPixCoords` 

286 Use the WCS information to add the pixel coordinates of each source 

287 Adds an ``x`` and ``y`` column to the catalog of fake sources. 

288 `trimFakeCat` 

289 Trim the fake cat to about the size of the input image. 

290 `mkFakeGalsimGalaxies` 

291 Use Galsim to make fake double sersic galaxies for each set of galaxy parameters in the input file. 

292 `mkFakeStars` 

293 Use the PSF information from the calexp to make a fake star using the magnitude information from the 

294 input file. 

295 `cleanCat` 

296 Remove rows of the input fake catalog which have half light radius, of either the bulge or the disk, 

297 that are 0. 

298 `addFakeSources` 

299 Add the fake sources to the calexp. 

300 

301 Notes 

302 ----- 

303 The ``calexp`` with fake souces added to it is written out as the datatype ``calexp_fakes``. 

304 """ 

305 

306 _DefaultName = "processCcdWithFakes" 

307 ConfigClass = ProcessCcdWithFakesConfig 

308 

309 def __init__(self, schema=None, **kwargs): 

310 """Initalize things! This should go above in the class docstring 

311 """ 

312 

313 super().__init__(**kwargs) 

314 

315 if schema is None: 

316 schema = SourceTable.makeMinimalSchema() 

317 self.schema = schema 

318 self.makeSubtask("insertFakes") 

319 self.makeSubtask("calibrate") 

320 

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

322 inputs = butlerQC.get(inputRefs) 

323 detectorId = inputs["exposure"].getInfo().getDetector().getId() 

324 

325 if 'idGenerator' not in inputs.keys(): 

326 inputs['idGenerator'] = self.config.idGenerator.apply(butlerQC.quantum.dataId) 

327 

328 expWcs = inputs["exposure"].getWcs() 

329 tractId = None 

330 if not self.config.doApplyExternalGlobalSkyWcs and not self.config.doApplyExternalTractSkyWcs: 

331 if expWcs is None: 

332 self.log.info("No WCS for exposure %s so cannot insert fake sources. Skipping detector.", 

333 butlerQC.quantum.dataId) 

334 return None 

335 else: 

336 inputs["wcs"] = expWcs 

337 elif self.config.doApplyExternalGlobalSkyWcs: 

338 externalSkyWcsCatalog = inputs["externalSkyWcsGlobalCatalog"] 

339 row = externalSkyWcsCatalog.find(detectorId) 

340 if row is None: 

341 self.log.info("No %s external global sky WCS for exposure %s so cannot insert fake " 

342 "sources. Skipping detector.", self.config.externalSkyWcsName, 

343 butlerQC.quantum.dataId) 

344 return None 

345 inputs["wcs"] = row.getWcs() 

346 elif self.config.doApplyExternalTractSkyWcs: 

347 externalSkyWcsCatalogList = inputs["externalSkyWcsTractCatalog"] 

348 if tractId is None: 

349 tractId = externalSkyWcsCatalogList[0].dataId["tract"] 

350 externalSkyWcsCatalog = None 

351 for externalSkyWcsCatalogRef in externalSkyWcsCatalogList: 

352 if externalSkyWcsCatalogRef.dataId["tract"] == tractId: 

353 externalSkyWcsCatalog = externalSkyWcsCatalogRef.get() 

354 break 

355 if externalSkyWcsCatalog is None: 

356 usedTract = externalSkyWcsCatalogList[-1].dataId["tract"] 

357 self.log.warning( 

358 f"Warning, external SkyWcs for tract {tractId} not found. Using tract {usedTract} " 

359 "instead.") 

360 externalSkyWcsCatalog = externalSkyWcsCatalogList[-1].get() 

361 row = externalSkyWcsCatalog.find(detectorId) 

362 if row is None: 

363 self.log.info("No %s external tract sky WCS for exposure %s so cannot insert fake " 

364 "sources. Skipping detector.", self.config.externalSkyWcsName, 

365 butlerQC.quantum.dataId) 

366 return None 

367 inputs["wcs"] = row.getWcs() 

368 

369 if not self.config.doApplyExternalGlobalPhotoCalib and not self.config.doApplyExternalTractPhotoCalib: 

370 inputs["photoCalib"] = inputs["exposure"].getPhotoCalib() 

371 elif self.config.doApplyExternalGlobalPhotoCalib: 

372 externalPhotoCalibCatalog = inputs["externalPhotoCalibGlobalCatalog"] 

373 row = externalPhotoCalibCatalog.find(detectorId) 

374 if row is None: 

375 self.log.info("No %s external global photoCalib for exposure %s so cannot insert fake " 

376 "sources. Skipping detector.", self.config.externalPhotoCalibName, 

377 butlerQC.quantum.dataId) 

378 return None 

379 inputs["photoCalib"] = row.getPhotoCalib() 

380 elif self.config.doApplyExternalTractPhotoCalib: 

381 externalPhotoCalibCatalogList = inputs["externalPhotoCalibTractCatalog"] 

382 if tractId is None: 

383 tractId = externalPhotoCalibCatalogList[0].dataId["tract"] 

384 externalPhotoCalibCatalog = None 

385 for externalPhotoCalibCatalogRef in externalPhotoCalibCatalogList: 

386 if externalPhotoCalibCatalogRef.dataId["tract"] == tractId: 

387 externalPhotoCalibCatalog = externalPhotoCalibCatalogRef.get() 

388 break 

389 if externalPhotoCalibCatalog is None: 

390 usedTract = externalPhotoCalibCatalogList[-1].dataId["tract"] 

391 self.log.warning( 

392 f"Warning, external PhotoCalib for tract {tractId} not found. Using tract {usedTract} " 

393 "instead.") 

394 externalPhotoCalibCatalog = externalPhotoCalibCatalogList[-1].get() 

395 row = externalPhotoCalibCatalog.find(detectorId) 

396 if row is None: 

397 self.log.info("No %s external tract photoCalib for exposure %s so cannot insert fake " 

398 "sources. Skipping detector.", self.config.externalPhotoCalibName, 

399 butlerQC.quantum.dataId) 

400 return None 

401 inputs["photoCalib"] = row.getPhotoCalib() 

402 

403 outputs = self.run(**inputs) 

404 butlerQC.put(outputs, outputRefs) 

405 

406 def run(self, fakeCats, exposure, skyMap, wcs=None, photoCalib=None, 

407 icSourceCat=None, sfdSourceCat=None, externalSkyWcsGlobalCatalog=None, 

408 externalSkyWcsTractCatalog=None, externalPhotoCalibGlobalCatalog=None, 

409 externalPhotoCalibTractCatalog=None, idGenerator=None): 

410 """Add fake sources to a calexp and then run detection, deblending and 

411 measurement. 

412 

413 Parameters 

414 ---------- 

415 fakeCats : `list` of `lsst.daf.butler.DeferredDatasetHandle` 

416 Set of tract level fake catalogs that potentially cover this 

417 detectorVisit. 

418 exposure : `lsst.afw.image.exposure.exposure.ExposureF` 

419 The exposure to add the fake sources to. 

420 skyMap : `lsst.skymap.SkyMap` 

421 SkyMap defining the tracts and patches the fakes are stored over. 

422 wcs : `lsst.afw.geom.SkyWcs`, optional 

423 WCS to use to add fake sources. 

424 photoCalib : `lsst.afw.image.photoCalib.PhotoCalib`, optional 

425 Photometric calibration to be used to calibrate the fake sources. 

426 icSourceCat : `lsst.afw.table.SourceCatalog`, optional 

427 Catalog to take the information about which sources were used for 

428 calibration from. 

429 sfdSourceCat : `lsst.afw.table.SourceCatalog`, optional 

430 Catalog produced by singleFrameDriver, needed to copy some 

431 calibration flags from. 

432 externalSkyWcsGlobalCatalog : `lsst.afw.table.ExposureCatalog`, \ 

433 optional 

434 Exposure catalog with external skyWcs to be applied per config. 

435 externalSkyWcsTractCatalog : `lsst.afw.table.ExposureCatalog`, optional 

436 Exposure catalog with external skyWcs to be applied per config. 

437 externalPhotoCalibGlobalCatalog : `lsst.afw.table.ExposureCatalog`, \ 

438 optional 

439 Exposure catalog with external photoCalib to be applied per config 

440 externalPhotoCalibTractCatalog : `lsst.afw.table.ExposureCatalog`, \ 

441 optional 

442 Exposure catalog with external photoCalib to be applied per config. 

443 idGenerator : `lsst.meas.base.IdGenerator`, optional 

444 Object that generates Source IDs and random seeds. 

445 

446 Returns 

447 ------- 

448 resultStruct : `lsst.pipe.base.struct.Struct` 

449 Result struct containing: 

450 

451 - outputExposure: `lsst.afw.image.exposure.exposure.ExposureF` 

452 - outputCat: `lsst.afw.table.source.source.SourceCatalog` 

453 

454 Notes 

455 ----- 

456 Adds pixel coordinates for each source to the fakeCat and removes 

457 objects with bulge or disk half light radius = 0 (if ``config.cleanCat 

458 = True``). These columns are called ``x`` and ``y`` and are in pixels. 

459 

460 Adds the ``Fake`` mask plane to the exposure which is then set by 

461 `addFakeSources` to mark where fake sources have been added. Uses the 

462 information in the ``fakeCat`` to make fake galaxies (using galsim) and 

463 fake stars, using the PSF models from the PSF information for the 

464 calexp. These are then added to the calexp and the calexp with fakes 

465 included returned. 

466 

467 The galsim galaxies are made using a double sersic profile, one for the 

468 bulge and one for the disk, this is then convolved with the PSF at that 

469 point. 

470 """ 

471 fakeCat = self.composeFakeCat(fakeCats, skyMap) 

472 

473 if wcs is None: 

474 wcs = exposure.getWcs() 

475 

476 if photoCalib is None: 

477 photoCalib = exposure.getPhotoCalib() 

478 

479 if self.config.doMatchVisit: 

480 fakeCat = self.getVisitMatchedFakeCat(fakeCat, exposure) 

481 

482 self.insertFakes.run(fakeCat, exposure, wcs, photoCalib) 

483 

484 # detect, deblend and measure sources 

485 if idGenerator is None: 

486 idGenerator = IdGenerator() 

487 returnedStruct = self.calibrate.run(exposure, idGenerator=idGenerator) 

488 sourceCat = returnedStruct.sourceCat 

489 

490 sourceCat = self.copyCalibrationFields(sfdSourceCat, sourceCat, self.config.srcFieldsToCopy) 

491 

492 resultStruct = pipeBase.Struct(outputExposure=exposure, outputCat=sourceCat) 

493 return resultStruct 

494 

495 def composeFakeCat(self, fakeCats, skyMap): 

496 """Concatenate the fakeCats from tracts that may cover the exposure. 

497 

498 Parameters 

499 ---------- 

500 fakeCats : `list` of `lsst.daf.butler.DeferredDatasetHandle` 

501 Set of fake cats to concatenate. 

502 skyMap : `lsst.skymap.SkyMap` 

503 SkyMap defining the geometry of the tracts and patches. 

504 

505 Returns 

506 ------- 

507 combinedFakeCat : `pandas.DataFrame` 

508 All fakes that cover the inner polygon of the tracts in this 

509 quantum. 

510 """ 

511 if len(fakeCats) == 1: 

512 return fakeCats[0].get() 

513 outputCat = [] 

514 for fakeCatRef in fakeCats: 

515 cat = fakeCatRef.get() 

516 tractId = fakeCatRef.dataId["tract"] 

517 # Make sure all data is within the inner part of the tract. 

518 outputCat.append(cat[ 

519 skyMap.findTractIdArray(cat[self.config.insertFakes.ra_col], 

520 cat[self.config.insertFakes.dec_col], 

521 degrees=False) 

522 == tractId]) 

523 

524 return pd.concat(outputCat) 

525 

526 def getVisitMatchedFakeCat(self, fakeCat, exposure): 

527 """Trim the fakeCat to select particular visit 

528 

529 Parameters 

530 ---------- 

531 fakeCat : `pandas.core.frame.DataFrame` 

532 The catalog of fake sources to add to the exposure 

533 exposure : `lsst.afw.image.exposure.exposure.ExposureF` 

534 The exposure to add the fake sources to 

535 

536 Returns 

537 ------- 

538 movingFakeCat : `pandas.DataFrame` 

539 All fakes that belong to the visit 

540 """ 

541 selected = exposure.getInfo().getVisitInfo().getId() == fakeCat["visit"] 

542 

543 return fakeCat[selected] 

544 

545 def copyCalibrationFields(self, calibCat, sourceCat, fieldsToCopy): 

546 """Match sources in calibCat and sourceCat and copy the specified fields 

547 

548 Parameters 

549 ---------- 

550 calibCat : `lsst.afw.table.SourceCatalog` 

551 Catalog from which to copy fields. 

552 sourceCat : `lsst.afw.table.SourceCatalog` 

553 Catalog to which to copy fields. 

554 fieldsToCopy : `lsst.pex.config.listField.List` 

555 Fields to copy from calibCat to SoourceCat. 

556 

557 Returns 

558 ------- 

559 newCat : `lsst.afw.table.SourceCatalog` 

560 Catalog which includes the copied fields. 

561 

562 The fields copied are those specified by `fieldsToCopy` that actually exist 

563 in the schema of `calibCat`. 

564 

565 This version was based on and adapted from the one in calibrateTask. 

566 """ 

567 

568 # Make a new SourceCatalog with the data from sourceCat so that we can add the new columns to it 

569 sourceSchemaMapper = afwTable.SchemaMapper(sourceCat.schema) 

570 sourceSchemaMapper.addMinimalSchema(sourceCat.schema, True) 

571 

572 calibSchemaMapper = afwTable.SchemaMapper(calibCat.schema, sourceCat.schema) 

573 

574 # Add the desired columns from the option fieldsToCopy 

575 missingFieldNames = [] 

576 for fieldName in fieldsToCopy: 

577 if fieldName in calibCat.schema: 

578 schemaItem = calibCat.schema.find(fieldName) 

579 calibSchemaMapper.editOutputSchema().addField(schemaItem.getField()) 

580 schema = calibSchemaMapper.editOutputSchema() 

581 calibSchemaMapper.addMapping(schemaItem.getKey(), schema.find(fieldName).getField()) 

582 else: 

583 missingFieldNames.append(fieldName) 

584 if missingFieldNames: 

585 raise RuntimeError(f"calibCat is missing fields {missingFieldNames} specified in " 

586 "fieldsToCopy") 

587 

588 if "calib_detected" not in calibSchemaMapper.getOutputSchema(): 

589 self.calibSourceKey = calibSchemaMapper.addOutputField(afwTable.Field["Flag"]("calib_detected", 

590 "Source was detected as an icSource")) 

591 else: 

592 self.calibSourceKey = None 

593 

594 schema = calibSchemaMapper.getOutputSchema() 

595 newCat = afwTable.SourceCatalog(schema) 

596 newCat.reserve(len(sourceCat)) 

597 newCat.extend(sourceCat, sourceSchemaMapper) 

598 

599 # Set the aliases so it doesn't complain. 

600 for k, v in sourceCat.schema.getAliasMap().items(): 

601 newCat.schema.getAliasMap().set(k, v) 

602 

603 select = newCat["deblend_nChild"] == 0 

604 matches = afwTable.matchXy(newCat[select], calibCat, self.config.matchRadiusPix) 

605 # Check that no sourceCat sources are listed twice (we already know 

606 # that each match has a unique calibCat source ID, due to using 

607 # that ID as the key in bestMatches) 

608 numMatches = len(matches) 

609 numUniqueSources = len(set(m[1].getId() for m in matches)) 

610 if numUniqueSources != numMatches: 

611 self.log.warning("%d calibCat sources matched only %d sourceCat sources", numMatches, 

612 numUniqueSources) 

613 

614 self.log.info("Copying flags from calibCat to sourceCat for %s sources", numMatches) 

615 

616 # For each match: set the calibSourceKey flag and copy the desired 

617 # fields 

618 for src, calibSrc, d in matches: 

619 if self.calibSourceKey: 

620 src.setFlag(self.calibSourceKey, True) 

621 # src.assign copies the footprint from calibSrc, which we don't want 

622 # (DM-407) 

623 # so set calibSrc's footprint to src's footprint before src.assign, 

624 # then restore it 

625 calibSrcFootprint = calibSrc.getFootprint() 

626 try: 

627 calibSrc.setFootprint(src.getFootprint()) 

628 src.assign(calibSrc, calibSchemaMapper) 

629 finally: 

630 calibSrc.setFootprint(calibSrcFootprint) 

631 

632 return newCat 

633 

634 

635class ProcessCcdWithVariableFakesConnections(ProcessCcdWithFakesConnections): 

636 ccdVisitFakeMagnitudes = cT.Output( 

637 doc="Catalog of fakes with magnitudes scattered for this ccdVisit.", 

638 name="{fakesType}ccdVisitFakeMagnitudes", 

639 storageClass="DataFrame", 

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

641 ) 

642 

643 

644@deprecated( 

645 reason="This task will be removed in v28.0 as it is replaced by `source_injection` tasks.", 

646 version="v28.0", 

647 category=FutureWarning, 

648) 

649class ProcessCcdWithVariableFakesConfig(ProcessCcdWithFakesConfig, 

650 pipelineConnections=ProcessCcdWithVariableFakesConnections): 

651 scatterSize = pexConfig.RangeField( 

652 dtype=float, 

653 default=0.4, 

654 min=0, 

655 max=100, 

656 doc="Amount of scatter to add to the visit magnitude for variable " 

657 "sources." 

658 ) 

659 

660 

661@deprecated( 

662 reason="This task will be removed in v28.0 as it is replaced by `source_injection` tasks.", 

663 version="v28.0", 

664 category=FutureWarning, 

665) 

666class ProcessCcdWithVariableFakesTask(ProcessCcdWithFakesTask): 

667 """As ProcessCcdWithFakes except add variablity to the fakes catalog 

668 magnitude in the observed band for this ccdVisit. 

669 

670 Additionally, write out the modified magnitudes to the Butler. 

671 """ 

672 

673 _DefaultName = "processCcdWithVariableFakes" 

674 ConfigClass = ProcessCcdWithVariableFakesConfig 

675 

676 def run(self, fakeCats, exposure, skyMap, wcs=None, photoCalib=None, 

677 icSourceCat=None, sfdSourceCat=None, idGenerator=None): 

678 """Add fake sources to a calexp and then run detection, deblending and 

679 measurement. 

680 

681 Parameters 

682 ---------- 

683 fakeCat : `pandas.core.frame.DataFrame` 

684 The catalog of fake sources to add to the exposure. 

685 exposure : `lsst.afw.image.exposure.exposure.ExposureF` 

686 The exposure to add the fake sources to. 

687 skyMap : `lsst.skymap.SkyMap` 

688 SkyMap defining the tracts and patches the fakes are stored over. 

689 wcs : `lsst.afw.geom.SkyWcs`, optional 

690 WCS to use to add fake sources. 

691 photoCalib : `lsst.afw.image.photoCalib.PhotoCalib`, optional 

692 Photometric calibration to be used to calibrate the fake sources. 

693 icSourceCat : `lsst.afw.table.SourceCatalog`, optional 

694 Catalog to take the information about which sources were used for 

695 calibration from. 

696 sfdSourceCat : `lsst.afw.table.SourceCatalog`, optional 

697 Catalog produced by singleFrameDriver, needed to copy some 

698 calibration flags from. 

699 idGenerator : `lsst.meas.base.IdGenerator`, optional 

700 Object that generates Source IDs and random seeds. 

701 

702 Returns 

703 ------- 

704 resultStruct : `lsst.pipe.base.struct.Struct` 

705 Results struct containing: 

706 

707 - outputExposure : Exposure with added fakes 

708 (`lsst.afw.image.exposure.exposure.ExposureF`) 

709 - outputCat : Catalog with detected fakes 

710 (`lsst.afw.table.source.source.SourceCatalog`) 

711 - ccdVisitFakeMagnitudes : Magnitudes that these fakes were 

712 inserted with after being scattered (`pandas.DataFrame`) 

713 

714 Notes 

715 ----- 

716 Adds pixel coordinates for each source to the fakeCat and removes 

717 objects with bulge or disk half light radius = 0 (if ``config.cleanCat 

718 = True``). These columns are called ``x`` and ``y`` and are in pixels. 

719 

720 Adds the ``Fake`` mask plane to the exposure which is then set by 

721 `addFakeSources` to mark where fake sources have been added. Uses the 

722 information in the ``fakeCat`` to make fake galaxies (using galsim) and 

723 fake stars, using the PSF models from the PSF information for the 

724 calexp. These are then added to the calexp and the calexp with fakes 

725 included returned. 

726 

727 The galsim galaxies are made using a double sersic profile, one for the 

728 bulge and one for the disk, this is then convolved with the PSF at that 

729 point. 

730 

731 

732 """ 

733 fakeCat = self.composeFakeCat(fakeCats, skyMap) 

734 

735 if wcs is None: 

736 wcs = exposure.getWcs() 

737 

738 if photoCalib is None: 

739 photoCalib = exposure.getPhotoCalib() 

740 

741 if idGenerator is None: 

742 idGenerator = IdGenerator() 

743 

744 band = exposure.getFilter().bandLabel 

745 ccdVisitMagnitudes = self.addVariability( 

746 fakeCat, 

747 band, 

748 exposure, 

749 photoCalib, 

750 idGenerator.catalog_id, 

751 ) 

752 

753 self.insertFakes.run(fakeCat, exposure, wcs, photoCalib) 

754 

755 # detect, deblend and measure sources 

756 returnedStruct = self.calibrate.run(exposure, idGenerator=idGenerator) 

757 sourceCat = returnedStruct.sourceCat 

758 

759 sourceCat = self.copyCalibrationFields(sfdSourceCat, sourceCat, self.config.srcFieldsToCopy) 

760 

761 resultStruct = pipeBase.Struct(outputExposure=exposure, 

762 outputCat=sourceCat, 

763 ccdVisitFakeMagnitudes=ccdVisitMagnitudes) 

764 return resultStruct 

765 

766 def addVariability(self, fakeCat, band, exposure, photoCalib, rngSeed): 

767 """Add scatter to the fake catalog visit magnitudes. 

768 

769 Currently just adds a simple Gaussian scatter around the static fake 

770 magnitude. This function could be modified to return any number of 

771 fake variability. 

772 

773 Parameters 

774 ---------- 

775 fakeCat : `pandas.DataFrame` 

776 Catalog of fakes to modify magnitudes of. 

777 band : `str` 

778 Current observing band to modify. 

779 exposure : `lsst.afw.image.ExposureF` 

780 Exposure fakes will be added to. 

781 photoCalib : `lsst.afw.image.PhotoCalib` 

782 Photometric calibration object of ``exposure``. 

783 rngSeed : `int` 

784 Random number generator seed. 

785 

786 Returns 

787 ------- 

788 dataFrame : `pandas.DataFrame` 

789 DataFrame containing the values of the magnitudes to that will 

790 be inserted into this ccdVisit. 

791 """ 

792 rng = np.random.default_rng(rngSeed) 

793 magScatter = rng.normal(loc=0, 

794 scale=self.config.scatterSize, 

795 size=len(fakeCat)) 

796 visitMagnitudes = fakeCat[self.insertFakes.config.mag_col % band] + magScatter 

797 fakeCat.loc[:, self.insertFakes.config.mag_col % band] = visitMagnitudes 

798 return pd.DataFrame(data={"variableMag": visitMagnitudes})