Coverage for python/lsst/pipe/tasks/insertFakes.py: 17%

Shortcuts on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

409 statements  

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# (http://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 <http://www.gnu.org/licenses/>. 

21 

22""" 

23Insert fakes into deepCoadds 

24""" 

25import galsim 

26from astropy.table import Table 

27import numpy as np 

28from astropy import units as u 

29 

30import lsst.geom as geom 

31import lsst.afw.image as afwImage 

32import lsst.afw.math as afwMath 

33import lsst.pex.config as pexConfig 

34import lsst.pipe.base as pipeBase 

35 

36from lsst.pipe.base import CmdLineTask, PipelineTask, PipelineTaskConfig, PipelineTaskConnections 

37import lsst.pipe.base.connectionTypes as cT 

38from lsst.pex.exceptions import LogicError, InvalidParameterError 

39from lsst.coadd.utils.coaddDataIdContainer import ExistingCoaddDataIdContainer 

40from lsst.geom import SpherePoint, radians, Box2D, Point2D 

41 

42__all__ = ["InsertFakesConfig", "InsertFakesTask"] 

43 

44 

45def _add_fake_sources(exposure, objects, calibFluxRadius=12.0, logger=None): 

46 """Add fake sources to the given exposure 

47 

48 Parameters 

49 ---------- 

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

51 The exposure into which the fake sources should be added 

52 objects : `typing.Iterator` [`tuple` ['lsst.geom.SpherePoint`, `galsim.GSObject`]] 

53 An iterator of tuples that contains (or generates) locations and object 

54 surface brightness profiles to inject. 

55 calibFluxRadius : `float`, optional 

56 Aperture radius (in pixels) used to define the calibration for this 

57 exposure+catalog. This is used to produce the correct instrumental fluxes 

58 within the radius. The value should match that of the field defined in 

59 slot_CalibFlux_instFlux. 

60 logger : `lsst.log.log.log.Log` or `logging.Logger`, optional 

61 Logger. 

62 """ 

63 exposure.mask.addMaskPlane("FAKE") 

64 bitmask = exposure.mask.getPlaneBitMask("FAKE") 

65 if logger: 

66 logger.info(f"Adding mask plane with bitmask {bitmask}") 

67 

68 wcs = exposure.getWcs() 

69 psf = exposure.getPsf() 

70 

71 bbox = exposure.getBBox() 

72 fullBounds = galsim.BoundsI(bbox.minX, bbox.maxX, bbox.minY, bbox.maxY) 

73 gsImg = galsim.Image(exposure.image.array, bounds=fullBounds) 

74 

75 pixScale = wcs.getPixelScale(bbox.getCenter()).asArcseconds() 

76 

77 for spt, gsObj in objects: 

78 pt = wcs.skyToPixel(spt) 

79 posd = galsim.PositionD(pt.x, pt.y) 

80 posi = galsim.PositionI(pt.x//1, pt.y//1) 

81 if logger: 

82 logger.debug(f"Adding fake source at {pt}") 

83 

84 mat = wcs.linearizePixelToSky(spt, geom.arcseconds).getMatrix() 

85 gsWCS = galsim.JacobianWCS(mat[0, 0], mat[0, 1], mat[1, 0], mat[1, 1]) 

86 

87 # This check is here because sometimes the WCS 

88 # is multivalued and objects that should not be 

89 # were being included. 

90 gsPixScale = np.sqrt(gsWCS.pixelArea()) 

91 if gsPixScale < pixScale/2 or gsPixScale > pixScale*2: 

92 continue 

93 

94 try: 

95 psfArr = psf.computeKernelImage(pt).array 

96 except InvalidParameterError: 

97 # Try mapping to nearest point contained in bbox. 

98 contained_pt = Point2D( 

99 np.clip(pt.x, bbox.minX, bbox.maxX), 

100 np.clip(pt.y, bbox.minY, bbox.maxY) 

101 ) 

102 if pt == contained_pt: # no difference, so skip immediately 

103 if logger: 

104 logger.infof( 

105 "Cannot compute Psf for object at {}; skipping", 

106 pt 

107 ) 

108 continue 

109 # otherwise, try again with new point 

110 try: 

111 psfArr = psf.computeKernelImage(contained_pt).array 

112 except InvalidParameterError: 

113 if logger: 

114 logger.infof( 

115 "Cannot compute Psf for object at {}; skipping", 

116 pt 

117 ) 

118 continue 

119 

120 apCorr = psf.computeApertureFlux(calibFluxRadius) 

121 psfArr /= apCorr 

122 gsPSF = galsim.InterpolatedImage(galsim.Image(psfArr), wcs=gsWCS) 

123 

124 conv = galsim.Convolve(gsObj, gsPSF) 

125 stampSize = conv.getGoodImageSize(gsWCS.minLinearScale()) 

126 subBounds = galsim.BoundsI(posi).withBorder(stampSize//2) 

127 subBounds &= fullBounds 

128 

129 if subBounds.area() > 0: 

130 subImg = gsImg[subBounds] 

131 offset = posd - subBounds.true_center 

132 # Note, for calexp injection, pixel is already part of the PSF and 

133 # for coadd injection, it's incorrect to include the output pixel. 

134 # So for both cases, we draw using method='no_pixel'. 

135 

136 conv.drawImage( 

137 subImg, 

138 add_to_image=True, 

139 offset=offset, 

140 wcs=gsWCS, 

141 method='no_pixel' 

142 ) 

143 

144 subBox = geom.Box2I( 

145 geom.Point2I(subBounds.xmin, subBounds.ymin), 

146 geom.Point2I(subBounds.xmax, subBounds.ymax) 

147 ) 

148 exposure[subBox].mask.array |= bitmask 

149 

150 

151def _isWCSGalsimDefault(wcs, hdr): 

152 """Decide if wcs = galsim.PixelScale(1.0) is explicitly present in header, 

153 or if it's just the galsim default. 

154 

155 Parameters 

156 ---------- 

157 wcs : galsim.BaseWCS 

158 Potentially default WCS. 

159 hdr : galsim.fits.FitsHeader 

160 Header as read in by galsim. 

161 

162 Returns 

163 ------- 

164 isDefault : bool 

165 True if default, False if explicitly set in header. 

166 """ 

167 if wcs != galsim.PixelScale(1.0): 

168 return False 

169 if hdr.get('GS_WCS') is not None: 

170 return False 

171 if hdr.get('CTYPE1', 'LINEAR') == 'LINEAR': 

172 return not any(k in hdr for k in ['CD1_1', 'CDELT1']) 

173 for wcs_type in galsim.fitswcs.fits_wcs_types: 

174 # If one of these succeeds, then assume result is explicit 

175 try: 

176 wcs_type._readHeader(hdr) 

177 return False 

178 except Exception: 

179 pass 

180 else: 

181 return not any(k in hdr for k in ['CD1_1', 'CDELT1']) 

182 

183 

184class InsertFakesConnections(PipelineTaskConnections, 

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

186 "fakesType": "fakes_"}, 

187 dimensions=("tract", "patch", "band", "skymap")): 

188 

189 image = cT.Input( 

190 doc="Image into which fakes are to be added.", 

191 name="{coaddName}Coadd", 

192 storageClass="ExposureF", 

193 dimensions=("tract", "patch", "band", "skymap") 

194 ) 

195 

196 fakeCat = cT.Input( 

197 doc="Catalog of fake sources to draw inputs from.", 

198 name="{fakesType}fakeSourceCat", 

199 storageClass="DataFrame", 

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

201 ) 

202 

203 imageWithFakes = cT.Output( 

204 doc="Image with fake sources added.", 

205 name="{fakesType}{coaddName}Coadd", 

206 storageClass="ExposureF", 

207 dimensions=("tract", "patch", "band", "skymap") 

208 ) 

209 

210 

211class InsertFakesConfig(PipelineTaskConfig, 

212 pipelineConnections=InsertFakesConnections): 

213 """Config for inserting fake sources 

214 """ 

215 

216 # Unchanged 

217 

218 doCleanCat = pexConfig.Field( 

219 doc="If true removes bad sources from the catalog.", 

220 dtype=bool, 

221 default=True, 

222 ) 

223 

224 fakeType = pexConfig.Field( 

225 doc="What type of fake catalog to use, snapshot (includes variability in the magnitudes calculated " 

226 "from the MJD of the image), static (no variability) or filename for a user defined fits" 

227 "catalog.", 

228 dtype=str, 

229 default="static", 

230 ) 

231 

232 calibFluxRadius = pexConfig.Field( 

233 doc="Aperture radius (in pixels) that was used to define the calibration for this image+catalog. " 

234 "This will be used to produce the correct instrumental fluxes within the radius. " 

235 "This value should match that of the field defined in slot_CalibFlux_instFlux.", 

236 dtype=float, 

237 default=12.0, 

238 ) 

239 

240 coaddName = pexConfig.Field( 

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

242 dtype=str, 

243 default="deep", 

244 ) 

245 

246 doSubSelectSources = pexConfig.Field( 

247 doc="Set to True if you wish to sub select sources to be input based on the value in the column" 

248 "set in the sourceSelectionColName config option.", 

249 dtype=bool, 

250 default=False 

251 ) 

252 

253 insertImages = pexConfig.Field( 

254 doc="Insert images directly? True or False.", 

255 dtype=bool, 

256 default=False, 

257 ) 

258 

259 insertOnlyStars = pexConfig.Field( 

260 doc="Insert only stars? True or False.", 

261 dtype=bool, 

262 default=False, 

263 ) 

264 

265 doProcessAllDataIds = pexConfig.Field( 

266 doc="If True, all input data IDs will be processed, even those containing no fake sources.", 

267 dtype=bool, 

268 default=False, 

269 ) 

270 

271 trimBuffer = pexConfig.Field( 

272 doc="Size of the pixel buffer surrounding the image. Only those fake sources with a centroid" 

273 "falling within the image+buffer region will be considered for fake source injection.", 

274 dtype=int, 

275 default=100, 

276 ) 

277 

278 sourceType = pexConfig.Field( 

279 doc="The column name for the source type used in the fake source catalog.", 

280 dtype=str, 

281 default="sourceType", 

282 ) 

283 

284 fits_alignment = pexConfig.ChoiceField( 

285 doc="How should injections from FITS files be aligned?", 

286 dtype=str, 

287 allowed={ 

288 "wcs": ( 

289 "Input image will be transformed such that the local WCS in " 

290 "the FITS header matches the local WCS in the target image. " 

291 "I.e., North, East, and angular distances in the input image " 

292 "will match North, East, and angular distances in the target " 

293 "image." 

294 ), 

295 "pixel": ( 

296 "Input image will _not_ be transformed. Up, right, and pixel " 

297 "distances in the input image will match up, right and pixel " 

298 "distances in the target image." 

299 ) 

300 }, 

301 default="pixel" 

302 ) 

303 

304 # New source catalog config variables 

305 

306 ra_col = pexConfig.Field( 

307 doc="Source catalog column name for RA (in radians).", 

308 dtype=str, 

309 default="ra", 

310 ) 

311 

312 dec_col = pexConfig.Field( 

313 doc="Source catalog column name for dec (in radians).", 

314 dtype=str, 

315 default="dec", 

316 ) 

317 

318 bulge_semimajor_col = pexConfig.Field( 

319 doc="Source catalog column name for the semimajor axis (in arcseconds) " 

320 "of the bulge half-light ellipse.", 

321 dtype=str, 

322 default="bulge_semimajor", 

323 ) 

324 

325 bulge_axis_ratio_col = pexConfig.Field( 

326 doc="Source catalog column name for the axis ratio of the bulge " 

327 "half-light ellipse.", 

328 dtype=str, 

329 default="bulge_axis_ratio", 

330 ) 

331 

332 bulge_pa_col = pexConfig.Field( 

333 doc="Source catalog column name for the position angle (measured from " 

334 "North through East in degrees) of the semimajor axis of the bulge " 

335 "half-light ellipse.", 

336 dtype=str, 

337 default="bulge_pa", 

338 ) 

339 

340 bulge_n_col = pexConfig.Field( 

341 doc="Source catalog column name for the Sersic index of the bulge.", 

342 dtype=str, 

343 default="bulge_n", 

344 ) 

345 

346 disk_semimajor_col = pexConfig.Field( 

347 doc="Source catalog column name for the semimajor axis (in arcseconds) " 

348 "of the disk half-light ellipse.", 

349 dtype=str, 

350 default="disk_semimajor", 

351 ) 

352 

353 disk_axis_ratio_col = pexConfig.Field( 

354 doc="Source catalog column name for the axis ratio of the disk " 

355 "half-light ellipse.", 

356 dtype=str, 

357 default="disk_axis_ratio", 

358 ) 

359 

360 disk_pa_col = pexConfig.Field( 

361 doc="Source catalog column name for the position angle (measured from " 

362 "North through East in degrees) of the semimajor axis of the disk " 

363 "half-light ellipse.", 

364 dtype=str, 

365 default="disk_pa", 

366 ) 

367 

368 disk_n_col = pexConfig.Field( 

369 doc="Source catalog column name for the Sersic index of the disk.", 

370 dtype=str, 

371 default="disk_n", 

372 ) 

373 

374 bulge_disk_flux_ratio_col = pexConfig.Field( 

375 doc="Source catalog column name for the bulge/disk flux ratio.", 

376 dtype=str, 

377 default="bulge_disk_flux_ratio", 

378 ) 

379 

380 mag_col = pexConfig.Field( 

381 doc="Source catalog column name template for magnitudes, in the format " 

382 "``filter name``_mag_col. E.g., if this config variable is set to " 

383 "``%s_mag``, then the i-band magnitude will be searched for in the " 

384 "``i_mag`` column of the source catalog.", 

385 dtype=str, 

386 default="%s_mag" 

387 ) 

388 

389 select_col = pexConfig.Field( 

390 doc="Source catalog column name to be used to select which sources to " 

391 "add.", 

392 dtype=str, 

393 default="select", 

394 ) 

395 

396 # Deprecated config variables 

397 

398 raColName = pexConfig.Field( 

399 doc="RA column name used in the fake source catalog.", 

400 dtype=str, 

401 default="raJ2000", 

402 deprecated="Use `ra_col` instead." 

403 ) 

404 

405 decColName = pexConfig.Field( 

406 doc="Dec. column name used in the fake source catalog.", 

407 dtype=str, 

408 default="decJ2000", 

409 deprecated="Use `dec_col` instead." 

410 ) 

411 

412 diskHLR = pexConfig.Field( 

413 doc="Column name for the disk half light radius used in the fake source catalog.", 

414 dtype=str, 

415 default="DiskHalfLightRadius", 

416 deprecated=( 

417 "Use `disk_semimajor_col`, `disk_axis_ratio_col`, and `disk_pa_col`" 

418 " to specify disk half-light ellipse." 

419 ) 

420 ) 

421 

422 aDisk = pexConfig.Field( 

423 doc="The column name for the semi major axis length of the disk component used in the fake source" 

424 "catalog.", 

425 dtype=str, 

426 default="a_d", 

427 deprecated=( 

428 "Use `disk_semimajor_col`, `disk_axis_ratio_col`, and `disk_pa_col`" 

429 " to specify disk half-light ellipse." 

430 ) 

431 ) 

432 

433 bDisk = pexConfig.Field( 

434 doc="The column name for the semi minor axis length of the disk component.", 

435 dtype=str, 

436 default="b_d", 

437 deprecated=( 

438 "Use `disk_semimajor_col`, `disk_axis_ratio_col`, and `disk_pa_col`" 

439 " to specify disk half-light ellipse." 

440 ) 

441 ) 

442 

443 paDisk = pexConfig.Field( 

444 doc="The column name for the PA of the disk component used in the fake source catalog.", 

445 dtype=str, 

446 default="pa_disk", 

447 deprecated=( 

448 "Use `disk_semimajor_col`, `disk_axis_ratio_col`, and `disk_pa_col`" 

449 " to specify disk half-light ellipse." 

450 ) 

451 ) 

452 

453 nDisk = pexConfig.Field( 

454 doc="The column name for the sersic index of the disk component used in the fake source catalog.", 

455 dtype=str, 

456 default="disk_n", 

457 deprecated="Use `disk_n_col` instead." 

458 ) 

459 

460 bulgeHLR = pexConfig.Field( 

461 doc="Column name for the bulge half light radius used in the fake source catalog.", 

462 dtype=str, 

463 default="BulgeHalfLightRadius", 

464 deprecated=( 

465 "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and " 

466 "`bulge_pa_col` to specify disk half-light ellipse." 

467 ) 

468 ) 

469 

470 aBulge = pexConfig.Field( 

471 doc="The column name for the semi major axis length of the bulge component.", 

472 dtype=str, 

473 default="a_b", 

474 deprecated=( 

475 "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and " 

476 "`bulge_pa_col` to specify disk half-light ellipse." 

477 ) 

478 ) 

479 

480 bBulge = pexConfig.Field( 

481 doc="The column name for the semi minor axis length of the bulge component used in the fake source " 

482 "catalog.", 

483 dtype=str, 

484 default="b_b", 

485 deprecated=( 

486 "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and " 

487 "`bulge_pa_col` to specify disk half-light ellipse." 

488 ) 

489 ) 

490 

491 paBulge = pexConfig.Field( 

492 doc="The column name for the PA of the bulge component used in the fake source catalog.", 

493 dtype=str, 

494 default="pa_bulge", 

495 deprecated=( 

496 "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and " 

497 "`bulge_pa_col` to specify disk half-light ellipse." 

498 ) 

499 ) 

500 

501 nBulge = pexConfig.Field( 

502 doc="The column name for the sersic index of the bulge component used in the fake source catalog.", 

503 dtype=str, 

504 default="bulge_n", 

505 deprecated="Use `bulge_n_col` instead." 

506 ) 

507 

508 magVar = pexConfig.Field( 

509 doc="The column name for the magnitude calculated taking variability into account. In the format " 

510 "``filter name``magVar, e.g. imagVar for the magnitude in the i band.", 

511 dtype=str, 

512 default="%smagVar", 

513 deprecated="Use `mag_col` instead." 

514 ) 

515 

516 sourceSelectionColName = pexConfig.Field( 

517 doc="The name of the column in the input fakes catalogue to be used to determine which sources to" 

518 "add, default is none and when this is used all sources are added.", 

519 dtype=str, 

520 default="templateSource", 

521 deprecated="Use `select_col` instead." 

522 ) 

523 

524 

525class InsertFakesTask(PipelineTask, CmdLineTask): 

526 """Insert fake objects into images. 

527 

528 Add fake stars and galaxies to the given image, read in through the dataRef. Galaxy parameters are read in 

529 from the specified file and then modelled using galsim. 

530 

531 `InsertFakesTask` has five functions that make images of the fake sources and then add them to the 

532 image. 

533 

534 `addPixCoords` 

535 Use the WCS information to add the pixel coordinates of each source. 

536 `mkFakeGalsimGalaxies` 

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

538 `mkFakeStars` 

539 Use the PSF information from the image to make a fake star using the magnitude information from the 

540 input file. 

541 `cleanCat` 

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

543 that are 0. Also removes rows that have Sersic index outside of galsim's allowed paramters. If 

544 the config option sourceSelectionColName is set then this function limits the catalog of input fakes 

545 to only those which are True in this column. 

546 `addFakeSources` 

547 Add the fake sources to the image. 

548 

549 """ 

550 

551 _DefaultName = "insertFakes" 

552 ConfigClass = InsertFakesConfig 

553 

554 def runDataRef(self, dataRef): 

555 """Read in/write out the required data products and add fake sources to the deepCoadd. 

556 

557 Parameters 

558 ---------- 

559 dataRef : `lsst.daf.persistence.butlerSubset.ButlerDataRef` 

560 Data reference defining the image to have fakes added to it 

561 Used to access the following data products: 

562 deepCoadd 

563 """ 

564 

565 self.log.info("Adding fakes to: tract: %d, patch: %s, filter: %s", 

566 dataRef.dataId["tract"], dataRef.dataId["patch"], dataRef.dataId["filter"]) 

567 

568 # To do: should it warn when asked to insert variable sources into the coadd 

569 

570 if self.config.fakeType == "static": 

571 fakeCat = dataRef.get("deepCoadd_fakeSourceCat").toDataFrame() 

572 # To do: DM-16254, the read and write of the fake catalogs will be changed once the new pipeline 

573 # task structure for ref cats is in place. 

574 self.fakeSourceCatType = "deepCoadd_fakeSourceCat" 

575 else: 

576 fakeCat = Table.read(self.config.fakeType).to_pandas() 

577 

578 coadd = dataRef.get("deepCoadd") 

579 wcs = coadd.getWcs() 

580 photoCalib = coadd.getPhotoCalib() 

581 

582 imageWithFakes = self.run(fakeCat, coadd, wcs, photoCalib) 

583 

584 dataRef.put(imageWithFakes.imageWithFakes, "fakes_deepCoadd") 

585 

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

587 inputs = butlerQC.get(inputRefs) 

588 inputs["wcs"] = inputs["image"].getWcs() 

589 inputs["photoCalib"] = inputs["image"].getPhotoCalib() 

590 

591 outputs = self.run(**inputs) 

592 butlerQC.put(outputs, outputRefs) 

593 

594 @classmethod 

595 def _makeArgumentParser(cls): 

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

597 parser.add_id_argument(name="--id", datasetType="deepCoadd", 

598 help="data IDs for the deepCoadd, e.g. --id tract=12345 patch=1,2 filter=r", 

599 ContainerClass=ExistingCoaddDataIdContainer) 

600 return parser 

601 

602 def run(self, fakeCat, image, wcs, photoCalib): 

603 """Add fake sources to an image. 

604 

605 Parameters 

606 ---------- 

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

608 The catalog of fake sources to be input 

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

610 The image into which the fake sources should be added 

611 wcs : `lsst.afw.geom.SkyWcs` 

612 WCS to use to add fake sources 

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

614 Photometric calibration to be used to calibrate the fake sources 

615 

616 Returns 

617 ------- 

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

619 contains : image : `lsst.afw.image.exposure.exposure.ExposureF` 

620 

621 Notes 

622 ----- 

623 Adds pixel coordinates for each source to the fakeCat and removes objects with bulge or disk half 

624 light radius = 0 (if ``config.doCleanCat = True``). 

625 

626 Adds the ``Fake`` mask plane to the image which is then set by `addFakeSources` to mark where fake 

627 sources have been added. Uses the information in the ``fakeCat`` to make fake galaxies (using galsim) 

628 and fake stars, using the PSF models from the PSF information for the image. These are then added to 

629 the image and the image with fakes included returned. 

630 

631 The galsim galaxies are made using a double sersic profile, one for the bulge and one for the disk, 

632 this is then convolved with the PSF at that point. 

633 """ 

634 # Attach overriding wcs and photoCalib to image, but retain originals 

635 # so we can reset at the end. 

636 origWcs = image.getWcs() 

637 origPhotoCalib = image.getPhotoCalib() 

638 image.setWcs(wcs) 

639 image.setPhotoCalib(photoCalib) 

640 

641 band = image.getFilterLabel().bandLabel 

642 fakeCat = self._standardizeColumns(fakeCat, band) 

643 

644 fakeCat = self.addPixCoords(fakeCat, image) 

645 fakeCat = self.trimFakeCat(fakeCat, image) 

646 

647 if len(fakeCat) > 0: 

648 if not self.config.insertImages: 

649 if isinstance(fakeCat[self.config.sourceType].iloc[0], str): 

650 galCheckVal = "galaxy" 

651 starCheckVal = "star" 

652 elif isinstance(fakeCat[self.config.sourceType].iloc[0], bytes): 

653 galCheckVal = b"galaxy" 

654 starCheckVal = b"star" 

655 elif isinstance(fakeCat[self.config.sourceType].iloc[0], (int, float)): 

656 galCheckVal = 1 

657 starCheckVal = 0 

658 else: 

659 raise TypeError( 

660 "sourceType column does not have required type, should be str, bytes or int" 

661 ) 

662 if self.config.doCleanCat: 

663 fakeCat = self.cleanCat(fakeCat, starCheckVal) 

664 

665 generator = self._generateGSObjectsFromCatalog(image, fakeCat, galCheckVal, starCheckVal) 

666 else: 

667 generator = self._generateGSObjectsFromImages(image, fakeCat) 

668 _add_fake_sources(image, generator, calibFluxRadius=self.config.calibFluxRadius, logger=self.log) 

669 elif len(fakeCat) == 0 and self.config.doProcessAllDataIds: 

670 self.log.warning("No fakes found for this dataRef; processing anyway.") 

671 image.mask.addMaskPlane("FAKE") 

672 else: 

673 raise RuntimeError("No fakes found for this dataRef.") 

674 

675 # restore original exposure WCS and photoCalib 

676 image.setWcs(origWcs) 

677 image.setPhotoCalib(origPhotoCalib) 

678 

679 resultStruct = pipeBase.Struct(imageWithFakes=image) 

680 

681 return resultStruct 

682 

683 def _standardizeColumns(self, fakeCat, band): 

684 """Use config variables to 'standardize' the expected columns and column 

685 names in the input catalog. 

686 

687 Parameters 

688 ---------- 

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

690 The catalog of fake sources to be input 

691 band : `str` 

692 Label for the current band being processed. 

693 

694 Returns 

695 ------- 

696 outCat : `pandas.core.frame.DataFrame` 

697 The standardized catalog of fake sources 

698 """ 

699 cfg = self.config 

700 replace_dict = {} 

701 

702 def add_to_replace_dict(new_name, depr_name, std_name): 

703 if new_name in fakeCat.columns: 

704 replace_dict[new_name] = std_name 

705 elif depr_name in fakeCat.columns: 

706 replace_dict[depr_name] = std_name 

707 else: 

708 raise ValueError(f"Could not determine column for {std_name}.") 

709 

710 # Prefer new config variables over deprecated config variables. 

711 # RA, dec, and mag are always required. Do these first 

712 for new_name, depr_name, std_name in [ 

713 (cfg.ra_col, cfg.raColName, 'ra'), 

714 (cfg.dec_col, cfg.decColName, 'dec'), 

715 (cfg.mag_col%band, cfg.magVar%band, 'mag') 

716 ]: 

717 add_to_replace_dict(new_name, depr_name, std_name) 

718 # Only handle bulge/disk params if not injecting images 

719 if not cfg.insertImages and not cfg.insertOnlyStars: 

720 for new_name, depr_name, std_name in [ 

721 (cfg.bulge_n_col, cfg.nBulge, 'bulge_n'), 

722 (cfg.bulge_pa_col, cfg.paBulge, 'bulge_pa'), 

723 (cfg.disk_n_col, cfg.nDisk, 'disk_n'), 

724 (cfg.disk_pa_col, cfg.paDisk, 'disk_pa'), 

725 ]: 

726 add_to_replace_dict(new_name, depr_name, std_name) 

727 

728 if cfg.doSubSelectSources: 

729 add_to_replace_dict( 

730 cfg.select_col, 

731 cfg.sourceSelectionColName, 

732 'select' 

733 ) 

734 fakeCat = fakeCat.rename(columns=replace_dict, copy=False) 

735 

736 # Handling the half-light radius and axis-ratio are trickier, since we 

737 # moved from expecting (HLR, a, b) to expecting (semimajor, axis_ratio). 

738 # Just handle these manually. 

739 if not cfg.insertImages and not cfg.insertOnlyStars: 

740 if ( 

741 cfg.bulge_semimajor_col in fakeCat.columns 

742 and cfg.bulge_axis_ratio_col in fakeCat.columns 

743 ): 

744 fakeCat = fakeCat.rename( 

745 columns={ 

746 cfg.bulge_semimajor_col: 'bulge_semimajor', 

747 cfg.bulge_axis_ratio_col: 'bulge_axis_ratio', 

748 cfg.disk_semimajor_col: 'disk_semimajor', 

749 cfg.disk_axis_ratio_col: 'disk_axis_ratio', 

750 }, 

751 copy=False 

752 ) 

753 elif ( 

754 cfg.bulgeHLR in fakeCat.columns 

755 and cfg.aBulge in fakeCat.columns 

756 and cfg.bBulge in fakeCat.columns 

757 ): 

758 fakeCat['bulge_axis_ratio'] = ( 

759 fakeCat[cfg.bBulge]/fakeCat[cfg.aBulge] 

760 ) 

761 fakeCat['bulge_semimajor'] = ( 

762 fakeCat[cfg.bulgeHLR]/np.sqrt(fakeCat['bulge_axis_ratio']) 

763 ) 

764 fakeCat['disk_axis_ratio'] = ( 

765 fakeCat[cfg.bDisk]/fakeCat[cfg.aDisk] 

766 ) 

767 fakeCat['disk_semimajor'] = ( 

768 fakeCat[cfg.diskHLR]/np.sqrt(fakeCat['disk_axis_ratio']) 

769 ) 

770 else: 

771 raise ValueError( 

772 "Could not determine columns for half-light radius and " 

773 "axis ratio." 

774 ) 

775 

776 # Process the bulge/disk flux ratio if possible. 

777 if cfg.bulge_disk_flux_ratio_col in fakeCat.columns: 

778 fakeCat = fakeCat.rename( 

779 columns={ 

780 cfg.bulge_disk_flux_ratio_col: 'bulge_disk_flux_ratio' 

781 }, 

782 copy=False 

783 ) 

784 else: 

785 fakeCat['bulge_disk_flux_ratio'] = 1.0 

786 

787 return fakeCat 

788 

789 def _generateGSObjectsFromCatalog(self, exposure, fakeCat, galCheckVal, starCheckVal): 

790 """Process catalog to generate `galsim.GSObject` s. 

791 

792 Parameters 

793 ---------- 

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

795 The exposure into which the fake sources should be added 

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

797 The catalog of fake sources to be input 

798 galCheckVal : `str`, `bytes` or `int` 

799 The value that is set in the sourceType column to specifiy an object is a galaxy. 

800 starCheckVal : `str`, `bytes` or `int` 

801 The value that is set in the sourceType column to specifiy an object is a star. 

802 

803 Yields 

804 ------ 

805 gsObjects : `generator` 

806 A generator of tuples of `lsst.geom.SpherePoint` and `galsim.GSObject`. 

807 """ 

808 wcs = exposure.getWcs() 

809 photoCalib = exposure.getPhotoCalib() 

810 

811 self.log.info("Making %d objects for insertion", len(fakeCat)) 

812 

813 for (index, row) in fakeCat.iterrows(): 

814 ra = row['ra'] 

815 dec = row['dec'] 

816 skyCoord = SpherePoint(ra, dec, radians) 

817 xy = wcs.skyToPixel(skyCoord) 

818 

819 try: 

820 flux = photoCalib.magnitudeToInstFlux(row['mag'], xy) 

821 except LogicError: 

822 continue 

823 

824 sourceType = row[self.config.sourceType] 

825 if sourceType == galCheckVal: 

826 # GalSim convention: HLR = sqrt(a * b) = a * sqrt(b / a) 

827 bulge_gs_HLR = row['bulge_semimajor']*np.sqrt(row['bulge_axis_ratio']) 

828 bulge = galsim.Sersic(n=row['bulge_n'], half_light_radius=bulge_gs_HLR) 

829 bulge = bulge.shear(q=row['bulge_axis_ratio'], beta=((90 - row['bulge_pa'])*galsim.degrees)) 

830 

831 disk_gs_HLR = row['disk_semimajor']*np.sqrt(row['disk_axis_ratio']) 

832 disk = galsim.Sersic(n=row['disk_n'], half_light_radius=disk_gs_HLR) 

833 disk = disk.shear(q=row['disk_axis_ratio'], beta=((90 - row['disk_pa'])*galsim.degrees)) 

834 

835 gal = bulge*row['bulge_disk_flux_ratio'] + disk 

836 gal = gal.withFlux(flux) 

837 

838 yield skyCoord, gal 

839 elif sourceType == starCheckVal: 

840 star = galsim.DeltaFunction() 

841 star = star.withFlux(flux) 

842 yield skyCoord, star 

843 else: 

844 raise TypeError(f"Unknown sourceType {sourceType}") 

845 

846 def _generateGSObjectsFromImages(self, exposure, fakeCat): 

847 """Process catalog to generate `galsim.GSObject` s. 

848 

849 Parameters 

850 ---------- 

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

852 The exposure into which the fake sources should be added 

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

854 The catalog of fake sources to be input 

855 

856 Yields 

857 ------ 

858 gsObjects : `generator` 

859 A generator of tuples of `lsst.geom.SpherePoint` and `galsim.GSObject`. 

860 """ 

861 band = exposure.getFilterLabel().bandLabel 

862 wcs = exposure.getWcs() 

863 photoCalib = exposure.getPhotoCalib() 

864 

865 self.log.info("Processing %d fake images", len(fakeCat)) 

866 

867 for (index, row) in fakeCat.iterrows(): 

868 ra = row['ra'] 

869 dec = row['dec'] 

870 skyCoord = SpherePoint(ra, dec, radians) 

871 xy = wcs.skyToPixel(skyCoord) 

872 

873 try: 

874 flux = photoCalib.magnitudeToInstFlux(row['mag'], xy) 

875 except LogicError: 

876 continue 

877 

878 imFile = row[band+"imFilename"] 

879 try: 

880 imFile = imFile.decode("utf-8") 

881 except AttributeError: 

882 pass 

883 imFile = imFile.strip() 

884 im = galsim.fits.read(imFile, read_header=True) 

885 

886 if self.config.fits_alignment == "wcs": 

887 # galsim.fits.read will always attach a WCS to its output. If it 

888 # can't find a WCS in the FITS header, then it defaults to 

889 # scale = 1.0 arcsec / pix. So if that's the scale, then we 

890 # need to check if it was explicitly set or if it's just the 

891 # default. If it's just the default then we should raise an 

892 # exception. 

893 if _isWCSGalsimDefault(im.wcs, im.header): 

894 raise RuntimeError( 

895 f"Cannot find WCS in input FITS file {imFile}" 

896 ) 

897 elif self.config.fits_alignment == "pixel": 

898 # Here we need to set im.wcs to the local WCS at the target 

899 # position. 

900 linWcs = wcs.linearizePixelToSky(skyCoord, geom.arcseconds) 

901 mat = linWcs.getMatrix() 

902 im.wcs = galsim.JacobianWCS( 

903 mat[0, 0], mat[0, 1], mat[1, 0], mat[1, 1] 

904 ) 

905 else: 

906 raise ValueError( 

907 f"Unknown fits_alignment type {self.config.fits_alignment}" 

908 ) 

909 

910 obj = galsim.InterpolatedImage(im, calculate_stepk=False) 

911 obj = obj.withFlux(flux) 

912 yield skyCoord, obj 

913 

914 def processImagesForInsertion(self, fakeCat, wcs, psf, photoCalib, band, pixelScale): 

915 """Process images from files into the format needed for insertion. 

916 

917 Parameters 

918 ---------- 

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

920 The catalog of fake sources to be input 

921 wcs : `lsst.afw.geom.skyWcs.skyWcs.SkyWc` 

922 WCS to use to add fake sources 

923 psf : `lsst.meas.algorithms.coaddPsf.coaddPsf.CoaddPsf` or 

924 `lsst.meas.extensions.psfex.psfexPsf.PsfexPsf` 

925 The PSF information to use to make the PSF images 

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

927 Photometric calibration to be used to calibrate the fake sources 

928 band : `str` 

929 The filter band that the observation was taken in. 

930 pixelScale : `float` 

931 The pixel scale of the image the sources are to be added to. 

932 

933 Returns 

934 ------- 

935 galImages : `list` 

936 A list of tuples of `lsst.afw.image.exposure.exposure.ExposureF` and 

937 `lsst.geom.Point2D` of their locations. 

938 For sources labelled as galaxy. 

939 starImages : `list` 

940 A list of tuples of `lsst.afw.image.exposure.exposure.ExposureF` and 

941 `lsst.geom.Point2D` of their locations. 

942 For sources labelled as star. 

943 

944 Notes 

945 ----- 

946 The input fakes catalog needs to contain the absolute path to the image in the 

947 band that is being used to add images to. It also needs to have the R.A. and 

948 declination of the fake source in radians and the sourceType of the object. 

949 """ 

950 galImages = [] 

951 starImages = [] 

952 

953 self.log.info("Processing %d fake images", len(fakeCat)) 

954 

955 for (imFile, sourceType, mag, x, y) in zip(fakeCat[band + "imFilename"].array, 

956 fakeCat["sourceType"].array, 

957 fakeCat['mag'].array, 

958 fakeCat["x"].array, fakeCat["y"].array): 

959 

960 im = afwImage.ImageF.readFits(imFile) 

961 

962 xy = geom.Point2D(x, y) 

963 

964 # We put these two PSF calculations within this same try block so that we catch cases 

965 # where the object's position is outside of the image. 

966 try: 

967 correctedFlux = psf.computeApertureFlux(self.config.calibFluxRadius, xy) 

968 psfKernel = psf.computeKernelImage(xy).getArray() 

969 psfKernel /= correctedFlux 

970 

971 except InvalidParameterError: 

972 self.log.info("%s at %0.4f, %0.4f outside of image", sourceType, x, y) 

973 continue 

974 

975 psfIm = galsim.InterpolatedImage(galsim.Image(psfKernel), scale=pixelScale) 

976 galsimIm = galsim.InterpolatedImage(galsim.Image(im.array), scale=pixelScale) 

977 convIm = galsim.Convolve([galsimIm, psfIm]) 

978 

979 try: 

980 outIm = convIm.drawImage(scale=pixelScale, method="real_space").array 

981 except (galsim.errors.GalSimFFTSizeError, MemoryError): 

982 continue 

983 

984 imSum = np.sum(outIm) 

985 divIm = outIm/imSum 

986 

987 try: 

988 flux = photoCalib.magnitudeToInstFlux(mag, xy) 

989 except LogicError: 

990 flux = 0 

991 

992 imWithFlux = flux*divIm 

993 

994 if sourceType == b"galaxy": 

995 galImages.append((afwImage.ImageF(imWithFlux), xy)) 

996 if sourceType == b"star": 

997 starImages.append((afwImage.ImageF(imWithFlux), xy)) 

998 

999 return galImages, starImages 

1000 

1001 def addPixCoords(self, fakeCat, image): 

1002 

1003 """Add pixel coordinates to the catalog of fakes. 

1004 

1005 Parameters 

1006 ---------- 

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

1008 The catalog of fake sources to be input 

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

1010 The image into which the fake sources should be added 

1011 

1012 Returns 

1013 ------- 

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

1015 """ 

1016 wcs = image.getWcs() 

1017 ras = fakeCat['ra'].values 

1018 decs = fakeCat['dec'].values 

1019 xs, ys = wcs.skyToPixelArray(ras, decs) 

1020 fakeCat["x"] = xs 

1021 fakeCat["y"] = ys 

1022 

1023 return fakeCat 

1024 

1025 def trimFakeCat(self, fakeCat, image): 

1026 """Trim the fake cat to the size of the input image plus trimBuffer padding. 

1027 

1028 `fakeCat` must be processed with addPixCoords before using this method. 

1029 

1030 Parameters 

1031 ---------- 

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

1033 The catalog of fake sources to be input 

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

1035 The image into which the fake sources should be added 

1036 

1037 Returns 

1038 ------- 

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

1040 The original fakeCat trimmed to the area of the image 

1041 """ 

1042 wideBbox = Box2D(image.getBBox()).dilatedBy(self.config.trimBuffer) 

1043 

1044 # prefilter in ra/dec to avoid cases where the wcs incorrectly maps 

1045 # input fakes which are really off the chip onto it. 

1046 ras = fakeCat[self.config.ra_col].values * u.rad 

1047 decs = fakeCat[self.config.dec_col].values * u.rad 

1048 

1049 isContainedRaDec = image.containsSkyCoords(ras, decs, padding=self.config.trimBuffer) 

1050 

1051 # also filter on the image BBox in pixel space 

1052 xs = fakeCat["x"].values 

1053 ys = fakeCat["y"].values 

1054 

1055 isContainedXy = xs >= wideBbox.minX 

1056 isContainedXy &= xs <= wideBbox.maxX 

1057 isContainedXy &= ys >= wideBbox.minY 

1058 isContainedXy &= ys <= wideBbox.maxY 

1059 

1060 return fakeCat[isContainedRaDec & isContainedXy] 

1061 

1062 def mkFakeGalsimGalaxies(self, fakeCat, band, photoCalib, pixelScale, psf, image): 

1063 """Make images of fake galaxies using GalSim. 

1064 

1065 Parameters 

1066 ---------- 

1067 band : `str` 

1068 pixelScale : `float` 

1069 psf : `lsst.meas.extensions.psfex.psfexPsf.PsfexPsf` 

1070 The PSF information to use to make the PSF images 

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

1072 The catalog of fake sources to be input 

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

1074 Photometric calibration to be used to calibrate the fake sources 

1075 

1076 Yields 

1077 ------- 

1078 galImages : `generator` 

1079 A generator of tuples of `lsst.afw.image.exposure.exposure.ExposureF` and 

1080 `lsst.geom.Point2D` of their locations. 

1081 

1082 Notes 

1083 ----- 

1084 

1085 Fake galaxies are made by combining two sersic profiles, one for the bulge and one for the disk. Each 

1086 component has an individual sersic index (n), a, b and position angle (PA). The combined profile is 

1087 then convolved with the PSF at the specified x, y position on the image. 

1088 

1089 The names of the columns in the ``fakeCat`` are configurable and are the column names from the 

1090 University of Washington simulations database as default. For more information see the doc strings 

1091 attached to the config options. 

1092 

1093 See mkFakeStars doc string for an explanation of calibration to instrumental flux. 

1094 """ 

1095 

1096 self.log.info("Making %d fake galaxy images", len(fakeCat)) 

1097 

1098 for (index, row) in fakeCat.iterrows(): 

1099 xy = geom.Point2D(row["x"], row["y"]) 

1100 

1101 # We put these two PSF calculations within this same try block so that we catch cases 

1102 # where the object's position is outside of the image. 

1103 try: 

1104 correctedFlux = psf.computeApertureFlux(self.config.calibFluxRadius, xy) 

1105 psfKernel = psf.computeKernelImage(xy).getArray() 

1106 psfKernel /= correctedFlux 

1107 

1108 except InvalidParameterError: 

1109 self.log.info("Galaxy at %0.4f, %0.4f outside of image", row["x"], row["y"]) 

1110 continue 

1111 

1112 try: 

1113 flux = photoCalib.magnitudeToInstFlux(row['mag'], xy) 

1114 except LogicError: 

1115 flux = 0 

1116 

1117 # GalSim convention: HLR = sqrt(a * b) = a * sqrt(b / a) 

1118 bulge_gs_HLR = row['bulge_semimajor']*np.sqrt(row['bulge_axis_ratio']) 

1119 bulge = galsim.Sersic(n=row['bulge_n'], half_light_radius=bulge_gs_HLR) 

1120 bulge = bulge.shear(q=row['bulge_axis_ratio'], beta=((90 - row['bulge_pa'])*galsim.degrees)) 

1121 

1122 disk_gs_HLR = row['disk_semimajor']*np.sqrt(row['disk_axis_ratio']) 

1123 disk = galsim.Sersic(n=row['disk_n'], half_light_radius=disk_gs_HLR) 

1124 disk = disk.shear(q=row['disk_axis_ratio'], beta=((90 - row['disk_pa'])*galsim.degrees)) 

1125 

1126 gal = bulge*row['bulge_disk_flux_ratio'] + disk 

1127 gal = gal.withFlux(flux) 

1128 

1129 psfIm = galsim.InterpolatedImage(galsim.Image(psfKernel), scale=pixelScale) 

1130 gal = galsim.Convolve([gal, psfIm]) 

1131 try: 

1132 galIm = gal.drawImage(scale=pixelScale, method="real_space").array 

1133 except (galsim.errors.GalSimFFTSizeError, MemoryError): 

1134 continue 

1135 

1136 yield (afwImage.ImageF(galIm), xy) 

1137 

1138 def mkFakeStars(self, fakeCat, band, photoCalib, psf, image): 

1139 

1140 """Make fake stars based off the properties in the fakeCat. 

1141 

1142 Parameters 

1143 ---------- 

1144 band : `str` 

1145 psf : `lsst.meas.extensions.psfex.psfexPsf.PsfexPsf` 

1146 The PSF information to use to make the PSF images 

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

1148 The catalog of fake sources to be input 

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

1150 The image into which the fake sources should be added 

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

1152 Photometric calibration to be used to calibrate the fake sources 

1153 

1154 Yields 

1155 ------- 

1156 starImages : `generator` 

1157 A generator of tuples of `lsst.afw.image.ImageF` of fake stars and 

1158 `lsst.geom.Point2D` of their locations. 

1159 

1160 Notes 

1161 ----- 

1162 To take a given magnitude and translate to the number of counts in the image 

1163 we use photoCalib.magnitudeToInstFlux, which returns the instrumental flux for the 

1164 given calibration radius used in the photometric calibration step. 

1165 Thus `calibFluxRadius` should be set to this same radius so that we can normalize 

1166 the PSF model to the correct instrumental flux within calibFluxRadius. 

1167 """ 

1168 

1169 self.log.info("Making %d fake star images", len(fakeCat)) 

1170 

1171 for (index, row) in fakeCat.iterrows(): 

1172 xy = geom.Point2D(row["x"], row["y"]) 

1173 

1174 # We put these two PSF calculations within this same try block so that we catch cases 

1175 # where the object's position is outside of the image. 

1176 try: 

1177 correctedFlux = psf.computeApertureFlux(self.config.calibFluxRadius, xy) 

1178 starIm = psf.computeImage(xy) 

1179 starIm /= correctedFlux 

1180 

1181 except InvalidParameterError: 

1182 self.log.info("Star at %0.4f, %0.4f outside of image", row["x"], row["y"]) 

1183 continue 

1184 

1185 try: 

1186 flux = photoCalib.magnitudeToInstFlux(row['mag'], xy) 

1187 except LogicError: 

1188 flux = 0 

1189 

1190 starIm *= flux 

1191 yield ((starIm.convertF(), xy)) 

1192 

1193 def cleanCat(self, fakeCat, starCheckVal): 

1194 """Remove rows from the fakes catalog which have HLR = 0 for either the buldge or disk component, 

1195 also remove galaxies that have Sersic index outside the galsim min and max 

1196 allowed (0.3 <= n <= 6.2). 

1197 

1198 Parameters 

1199 ---------- 

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

1201 The catalog of fake sources to be input 

1202 starCheckVal : `str`, `bytes` or `int` 

1203 The value that is set in the sourceType column to specifiy an object is a star. 

1204 

1205 Returns 

1206 ------- 

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

1208 The input catalog of fake sources but with the bad objects removed 

1209 """ 

1210 

1211 rowsToKeep = (((fakeCat['bulge_semimajor'] != 0.0) & (fakeCat['disk_semimajor'] != 0.0)) 

1212 | (fakeCat[self.config.sourceType] == starCheckVal)) 

1213 numRowsNotUsed = len(fakeCat) - len(np.where(rowsToKeep)[0]) 

1214 self.log.info("Removing %d rows with HLR = 0 for either the bulge or disk", numRowsNotUsed) 

1215 fakeCat = fakeCat[rowsToKeep] 

1216 

1217 minN = galsim.Sersic._minimum_n 

1218 maxN = galsim.Sersic._maximum_n 

1219 rowsWithGoodSersic = (((fakeCat['bulge_n'] >= minN) & (fakeCat['bulge_n'] <= maxN) 

1220 & (fakeCat['disk_n'] >= minN) & (fakeCat['disk_n'] <= maxN)) 

1221 | (fakeCat[self.config.sourceType] == starCheckVal)) 

1222 numRowsNotUsed = len(fakeCat) - len(np.where(rowsWithGoodSersic)[0]) 

1223 self.log.info("Removing %d rows of galaxies with nBulge or nDisk outside of %0.2f <= n <= %0.2f", 

1224 numRowsNotUsed, minN, maxN) 

1225 fakeCat = fakeCat[rowsWithGoodSersic] 

1226 

1227 if self.config.doSubSelectSources: 

1228 numRowsNotUsed = len(fakeCat) - len(fakeCat['select']) 

1229 self.log.info("Removing %d rows which were not designated as template sources", numRowsNotUsed) 

1230 fakeCat = fakeCat[fakeCat['select']] 

1231 

1232 return fakeCat 

1233 

1234 def addFakeSources(self, image, fakeImages, sourceType): 

1235 """Add the fake sources to the given image 

1236 

1237 Parameters 

1238 ---------- 

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

1240 The image into which the fake sources should be added 

1241 fakeImages : `typing.Iterator` [`tuple` ['lsst.afw.image.ImageF`, `lsst.geom.Point2d`]] 

1242 An iterator of tuples that contains (or generates) images of fake sources, 

1243 and the locations they are to be inserted at. 

1244 sourceType : `str` 

1245 The type (star/galaxy) of fake sources input 

1246 

1247 Returns 

1248 ------- 

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

1250 

1251 Notes 

1252 ----- 

1253 Uses the x, y information in the ``fakeCat`` to position an image of the fake interpolated onto the 

1254 pixel grid of the image. Sets the ``FAKE`` mask plane for the pixels added with the fake source. 

1255 """ 

1256 

1257 imageBBox = image.getBBox() 

1258 imageMI = image.maskedImage 

1259 

1260 for (fakeImage, xy) in fakeImages: 

1261 X0 = xy.getX() - fakeImage.getWidth()/2 + 0.5 

1262 Y0 = xy.getY() - fakeImage.getHeight()/2 + 0.5 

1263 self.log.debug("Adding fake source at %d, %d", xy.getX(), xy.getY()) 

1264 if sourceType == "galaxy": 

1265 interpFakeImage = afwMath.offsetImage(fakeImage, X0, Y0, "lanczos3") 

1266 else: 

1267 interpFakeImage = fakeImage 

1268 

1269 interpFakeImBBox = interpFakeImage.getBBox() 

1270 interpFakeImBBox.clip(imageBBox) 

1271 

1272 if interpFakeImBBox.getArea() > 0: 

1273 imageMIView = imageMI[interpFakeImBBox] 

1274 clippedFakeImage = interpFakeImage[interpFakeImBBox] 

1275 clippedFakeImageMI = afwImage.MaskedImageF(clippedFakeImage) 

1276 clippedFakeImageMI.mask.set(self.bitmask) 

1277 imageMIView += clippedFakeImageMI 

1278 

1279 return image 

1280 

1281 def _getMetadataName(self): 

1282 """Disable metadata writing""" 

1283 return None