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

405 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 

28 

29import lsst.geom as geom 

30import lsst.afw.image as afwImage 

31import lsst.afw.math as afwMath 

32import lsst.pex.config as pexConfig 

33import lsst.pipe.base as pipeBase 

34 

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

36import lsst.pipe.base.connectionTypes as cT 

37from lsst.pex.exceptions import LogicError, InvalidParameterError 

38from lsst.coadd.utils.coaddDataIdContainer import ExistingCoaddDataIdContainer 

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

40 

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

42 

43 

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

45 """Add fake sources to the given exposure 

46 

47 Parameters 

48 ---------- 

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

50 The exposure into which the fake sources should be added 

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

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

53 surface brightness profiles to inject. 

54 calibFluxRadius : `float`, optional 

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

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

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

58 slot_CalibFlux_instFlux. 

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

60 Logger. 

61 """ 

62 exposure.mask.addMaskPlane("FAKE") 

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

64 if logger: 

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

66 

67 wcs = exposure.getWcs() 

68 psf = exposure.getPsf() 

69 

70 bbox = exposure.getBBox() 

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

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

73 

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

75 

76 for spt, gsObj in objects: 

77 pt = wcs.skyToPixel(spt) 

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

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

80 if logger: 

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

82 

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

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

85 

86 # This check is here because sometimes the WCS 

87 # is multivalued and objects that should not be 

88 # were being included. 

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

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

91 continue 

92 

93 try: 

94 psfArr = psf.computeKernelImage(pt).array 

95 except InvalidParameterError: 

96 # Try mapping to nearest point contained in bbox. 

97 contained_pt = Point2D( 

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

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

100 ) 

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

102 if logger: 

103 logger.infof( 

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

105 pt 

106 ) 

107 continue 

108 # otherwise, try again with new point 

109 try: 

110 psfArr = psf.computeKernelImage(contained_pt).array 

111 except InvalidParameterError: 

112 if logger: 

113 logger.infof( 

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

115 pt 

116 ) 

117 continue 

118 

119 apCorr = psf.computeApertureFlux(calibFluxRadius) 

120 psfArr /= apCorr 

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

122 

123 conv = galsim.Convolve(gsObj, gsPSF) 

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

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

126 subBounds &= fullBounds 

127 

128 if subBounds.area() > 0: 

129 subImg = gsImg[subBounds] 

130 offset = posd - subBounds.true_center 

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

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

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

134 

135 conv.drawImage( 

136 subImg, 

137 add_to_image=True, 

138 offset=offset, 

139 wcs=gsWCS, 

140 method='no_pixel' 

141 ) 

142 

143 subBox = geom.Box2I( 

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

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

146 ) 

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

148 

149 

150def _isWCSGalsimDefault(wcs, hdr): 

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

152 or if it's just the galsim default. 

153 

154 Parameters 

155 ---------- 

156 wcs : galsim.BaseWCS 

157 Potentially default WCS. 

158 hdr : galsim.fits.FitsHeader 

159 Header as read in by galsim. 

160 

161 Returns 

162 ------- 

163 isDefault : bool 

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

165 """ 

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

167 return False 

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

169 return False 

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

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

172 for wcs_type in galsim.fitswcs.fits_wcs_types: 

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

174 try: 

175 wcs_type._readHeader(hdr) 

176 return False 

177 except Exception: 

178 pass 

179 else: 

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

181 

182 

183class InsertFakesConnections(PipelineTaskConnections, 

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

185 "fakesType": "fakes_"}, 

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

187 

188 image = cT.Input( 

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

190 name="{coaddName}Coadd", 

191 storageClass="ExposureF", 

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

193 ) 

194 

195 fakeCat = cT.Input( 

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

197 name="{fakesType}fakeSourceCat", 

198 storageClass="DataFrame", 

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

200 ) 

201 

202 imageWithFakes = cT.Output( 

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

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

205 storageClass="ExposureF", 

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

207 ) 

208 

209 

210class InsertFakesConfig(PipelineTaskConfig, 

211 pipelineConnections=InsertFakesConnections): 

212 """Config for inserting fake sources 

213 """ 

214 

215 # Unchanged 

216 

217 doCleanCat = pexConfig.Field( 

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

219 dtype=bool, 

220 default=True, 

221 ) 

222 

223 fakeType = pexConfig.Field( 

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

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

226 "catalog.", 

227 dtype=str, 

228 default="static", 

229 ) 

230 

231 calibFluxRadius = pexConfig.Field( 

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

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

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

235 dtype=float, 

236 default=12.0, 

237 ) 

238 

239 coaddName = pexConfig.Field( 

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

241 dtype=str, 

242 default="deep", 

243 ) 

244 

245 doSubSelectSources = pexConfig.Field( 

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

247 "set in the sourceSelectionColName config option.", 

248 dtype=bool, 

249 default=False 

250 ) 

251 

252 insertImages = pexConfig.Field( 

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

254 dtype=bool, 

255 default=False, 

256 ) 

257 

258 insertOnlyStars = pexConfig.Field( 

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

260 dtype=bool, 

261 default=False, 

262 ) 

263 

264 doProcessAllDataIds = pexConfig.Field( 

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

266 dtype=bool, 

267 default=False, 

268 ) 

269 

270 trimBuffer = pexConfig.Field( 

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

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

273 dtype=int, 

274 default=100, 

275 ) 

276 

277 sourceType = pexConfig.Field( 

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

279 dtype=str, 

280 default="sourceType", 

281 ) 

282 

283 fits_alignment = pexConfig.ChoiceField( 

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

285 dtype=str, 

286 allowed={ 

287 "wcs": ( 

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

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

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

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

292 "image." 

293 ), 

294 "pixel": ( 

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

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

297 "distances in the target image." 

298 ) 

299 }, 

300 default="pixel" 

301 ) 

302 

303 # New source catalog config variables 

304 

305 ra_col = pexConfig.Field( 

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

307 dtype=str, 

308 default="ra", 

309 ) 

310 

311 dec_col = pexConfig.Field( 

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

313 dtype=str, 

314 default="dec", 

315 ) 

316 

317 bulge_semimajor_col = pexConfig.Field( 

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

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

320 dtype=str, 

321 default="bulge_semimajor", 

322 ) 

323 

324 bulge_axis_ratio_col = pexConfig.Field( 

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

326 "half-light ellipse.", 

327 dtype=str, 

328 default="bulge_axis_ratio", 

329 ) 

330 

331 bulge_pa_col = pexConfig.Field( 

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

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

334 "half-light ellipse.", 

335 dtype=str, 

336 default="bulge_pa", 

337 ) 

338 

339 bulge_n_col = pexConfig.Field( 

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

341 dtype=str, 

342 default="bulge_n", 

343 ) 

344 

345 disk_semimajor_col = pexConfig.Field( 

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

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

348 dtype=str, 

349 default="disk_semimajor", 

350 ) 

351 

352 disk_axis_ratio_col = pexConfig.Field( 

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

354 "half-light ellipse.", 

355 dtype=str, 

356 default="disk_axis_ratio", 

357 ) 

358 

359 disk_pa_col = pexConfig.Field( 

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

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

362 "half-light ellipse.", 

363 dtype=str, 

364 default="disk_pa", 

365 ) 

366 

367 disk_n_col = pexConfig.Field( 

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

369 dtype=str, 

370 default="disk_n", 

371 ) 

372 

373 bulge_disk_flux_ratio_col = pexConfig.Field( 

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

375 dtype=str, 

376 default="bulge_disk_flux_ratio", 

377 ) 

378 

379 mag_col = pexConfig.Field( 

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

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

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

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

384 dtype=str, 

385 default="%s_mag" 

386 ) 

387 

388 select_col = pexConfig.Field( 

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

390 "add.", 

391 dtype=str, 

392 default="select", 

393 ) 

394 

395 # Deprecated config variables 

396 

397 raColName = pexConfig.Field( 

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

399 dtype=str, 

400 default="raJ2000", 

401 deprecated="Use `ra_col` instead." 

402 ) 

403 

404 decColName = pexConfig.Field( 

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

406 dtype=str, 

407 default="decJ2000", 

408 deprecated="Use `dec_col` instead." 

409 ) 

410 

411 diskHLR = pexConfig.Field( 

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

413 dtype=str, 

414 default="DiskHalfLightRadius", 

415 deprecated=( 

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

417 " to specify disk half-light ellipse." 

418 ) 

419 ) 

420 

421 aDisk = pexConfig.Field( 

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

423 "catalog.", 

424 dtype=str, 

425 default="a_d", 

426 deprecated=( 

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

428 " to specify disk half-light ellipse." 

429 ) 

430 ) 

431 

432 bDisk = pexConfig.Field( 

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

434 dtype=str, 

435 default="b_d", 

436 deprecated=( 

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

438 " to specify disk half-light ellipse." 

439 ) 

440 ) 

441 

442 paDisk = pexConfig.Field( 

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

444 dtype=str, 

445 default="pa_disk", 

446 deprecated=( 

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

448 " to specify disk half-light ellipse." 

449 ) 

450 ) 

451 

452 nDisk = pexConfig.Field( 

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

454 dtype=str, 

455 default="disk_n", 

456 deprecated="Use `disk_n_col` instead." 

457 ) 

458 

459 bulgeHLR = pexConfig.Field( 

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

461 dtype=str, 

462 default="BulgeHalfLightRadius", 

463 deprecated=( 

464 "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and " 

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

466 ) 

467 ) 

468 

469 aBulge = pexConfig.Field( 

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

471 dtype=str, 

472 default="a_b", 

473 deprecated=( 

474 "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and " 

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

476 ) 

477 ) 

478 

479 bBulge = pexConfig.Field( 

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

481 "catalog.", 

482 dtype=str, 

483 default="b_b", 

484 deprecated=( 

485 "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and " 

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

487 ) 

488 ) 

489 

490 paBulge = pexConfig.Field( 

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

492 dtype=str, 

493 default="pa_bulge", 

494 deprecated=( 

495 "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and " 

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

497 ) 

498 ) 

499 

500 nBulge = pexConfig.Field( 

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

502 dtype=str, 

503 default="bulge_n", 

504 deprecated="Use `bulge_n_col` instead." 

505 ) 

506 

507 magVar = pexConfig.Field( 

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

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

510 dtype=str, 

511 default="%smagVar", 

512 deprecated="Use `mag_col` instead." 

513 ) 

514 

515 sourceSelectionColName = pexConfig.Field( 

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

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

518 dtype=str, 

519 default="templateSource", 

520 deprecated="Use `select_col` instead." 

521 ) 

522 

523 

524class InsertFakesTask(PipelineTask, CmdLineTask): 

525 """Insert fake objects into images. 

526 

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

528 from the specified file and then modelled using galsim. 

529 

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

531 image. 

532 

533 `addPixCoords` 

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

535 `mkFakeGalsimGalaxies` 

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

537 `mkFakeStars` 

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

539 input file. 

540 `cleanCat` 

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

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

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

544 to only those which are True in this column. 

545 `addFakeSources` 

546 Add the fake sources to the image. 

547 

548 """ 

549 

550 _DefaultName = "insertFakes" 

551 ConfigClass = InsertFakesConfig 

552 

553 def runDataRef(self, dataRef): 

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

555 

556 Parameters 

557 ---------- 

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

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

560 Used to access the following data products: 

561 deepCoadd 

562 """ 

563 

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

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

566 

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

568 

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

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

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

572 # task structure for ref cats is in place. 

573 self.fakeSourceCatType = "deepCoadd_fakeSourceCat" 

574 else: 

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

576 

577 coadd = dataRef.get("deepCoadd") 

578 wcs = coadd.getWcs() 

579 photoCalib = coadd.getPhotoCalib() 

580 

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

582 

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

584 

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

586 inputs = butlerQC.get(inputRefs) 

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

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

589 

590 outputs = self.run(**inputs) 

591 butlerQC.put(outputs, outputRefs) 

592 

593 @classmethod 

594 def _makeArgumentParser(cls): 

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

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

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

598 ContainerClass=ExistingCoaddDataIdContainer) 

599 return parser 

600 

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

602 """Add fake sources to an image. 

603 

604 Parameters 

605 ---------- 

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

607 The catalog of fake sources to be input 

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

609 The image into which the fake sources should be added 

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

611 WCS to use to add fake sources 

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

613 Photometric calibration to be used to calibrate the fake sources 

614 

615 Returns 

616 ------- 

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

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

619 

620 Notes 

621 ----- 

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

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

624 

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

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

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

628 the image and the image with fakes included returned. 

629 

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

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

632 """ 

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

634 # so we can reset at the end. 

635 origWcs = image.getWcs() 

636 origPhotoCalib = image.getPhotoCalib() 

637 image.setWcs(wcs) 

638 image.setPhotoCalib(photoCalib) 

639 

640 band = image.getFilterLabel().bandLabel 

641 fakeCat = self._standardizeColumns(fakeCat, band) 

642 

643 fakeCat = self.addPixCoords(fakeCat, image) 

644 fakeCat = self.trimFakeCat(fakeCat, image) 

645 

646 if len(fakeCat) > 0: 

647 if not self.config.insertImages: 

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

649 galCheckVal = "galaxy" 

650 starCheckVal = "star" 

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

652 galCheckVal = b"galaxy" 

653 starCheckVal = b"star" 

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

655 galCheckVal = 1 

656 starCheckVal = 0 

657 else: 

658 raise TypeError( 

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

660 ) 

661 if self.config.doCleanCat: 

662 fakeCat = self.cleanCat(fakeCat, starCheckVal) 

663 

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

665 else: 

666 generator = self._generateGSObjectsFromImages(image, fakeCat) 

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

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

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

670 image.mask.addMaskPlane("FAKE") 

671 else: 

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

673 

674 # restore original exposure WCS and photoCalib 

675 image.setWcs(origWcs) 

676 image.setPhotoCalib(origPhotoCalib) 

677 

678 resultStruct = pipeBase.Struct(imageWithFakes=image) 

679 

680 return resultStruct 

681 

682 def _standardizeColumns(self, fakeCat, band): 

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

684 names in the input catalog. 

685 

686 Parameters 

687 ---------- 

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

689 The catalog of fake sources to be input 

690 band : `str` 

691 Label for the current band being processed. 

692 

693 Returns 

694 ------- 

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

696 The standardized catalog of fake sources 

697 """ 

698 cfg = self.config 

699 replace_dict = {} 

700 

701 def add_to_replace_dict(new_name, depr_name, std_name): 

702 if new_name in fakeCat.columns: 

703 replace_dict[new_name] = std_name 

704 elif depr_name in fakeCat.columns: 

705 replace_dict[depr_name] = std_name 

706 else: 

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

708 

709 # Prefer new config variables over deprecated config variables. 

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

711 for new_name, depr_name, std_name in [ 

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

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

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

715 ]: 

716 add_to_replace_dict(new_name, depr_name, std_name) 

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

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

719 for new_name, depr_name, std_name in [ 

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

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

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

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

724 ]: 

725 add_to_replace_dict(new_name, depr_name, std_name) 

726 

727 if cfg.doSubSelectSources: 

728 add_to_replace_dict( 

729 cfg.select_col, 

730 cfg.sourceSelectionColName, 

731 'select' 

732 ) 

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

734 

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

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

737 # Just handle these manually. 

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

739 if ( 

740 cfg.bulge_semimajor_col in fakeCat.columns 

741 and cfg.bulge_axis_ratio_col in fakeCat.columns 

742 ): 

743 fakeCat = fakeCat.rename( 

744 columns={ 

745 cfg.bulge_semimajor_col: 'bulge_semimajor', 

746 cfg.bulge_axis_ratio_col: 'bulge_axis_ratio', 

747 cfg.disk_semimajor_col: 'disk_semimajor', 

748 cfg.disk_axis_ratio_col: 'disk_axis_ratio', 

749 }, 

750 copy=False 

751 ) 

752 elif ( 

753 cfg.bulgeHLR in fakeCat.columns 

754 and cfg.aBulge in fakeCat.columns 

755 and cfg.bBulge in fakeCat.columns 

756 ): 

757 fakeCat['bulge_axis_ratio'] = ( 

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

759 ) 

760 fakeCat['bulge_semimajor'] = ( 

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

762 ) 

763 fakeCat['disk_axis_ratio'] = ( 

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

765 ) 

766 fakeCat['disk_semimajor'] = ( 

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

768 ) 

769 else: 

770 raise ValueError( 

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

772 "axis ratio." 

773 ) 

774 

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

776 if cfg.bulge_disk_flux_ratio_col in fakeCat.columns: 

777 fakeCat = fakeCat.rename( 

778 columns={ 

779 cfg.bulge_disk_flux_ratio_col: 'bulge_disk_flux_ratio' 

780 }, 

781 copy=False 

782 ) 

783 else: 

784 fakeCat['bulge_disk_flux_ratio'] = 1.0 

785 

786 return fakeCat 

787 

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

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

790 

791 Parameters 

792 ---------- 

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

794 The exposure into which the fake sources should be added 

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

796 The catalog of fake sources to be input 

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

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

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

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

801 

802 Yields 

803 ------ 

804 gsObjects : `generator` 

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

806 """ 

807 wcs = exposure.getWcs() 

808 photoCalib = exposure.getPhotoCalib() 

809 

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

811 

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

813 ra = row['ra'] 

814 dec = row['dec'] 

815 skyCoord = SpherePoint(ra, dec, radians) 

816 xy = wcs.skyToPixel(skyCoord) 

817 

818 try: 

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

820 except LogicError: 

821 continue 

822 

823 sourceType = row[self.config.sourceType] 

824 if sourceType == galCheckVal: 

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

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

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

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

829 

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

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

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

833 

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

835 gal = gal.withFlux(flux) 

836 

837 yield skyCoord, gal 

838 elif sourceType == starCheckVal: 

839 star = galsim.DeltaFunction() 

840 star = star.withFlux(flux) 

841 yield skyCoord, star 

842 else: 

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

844 

845 def _generateGSObjectsFromImages(self, exposure, fakeCat): 

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

847 

848 Parameters 

849 ---------- 

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

851 The exposure into which the fake sources should be added 

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

853 The catalog of fake sources to be input 

854 

855 Yields 

856 ------ 

857 gsObjects : `generator` 

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

859 """ 

860 band = exposure.getFilterLabel().bandLabel 

861 wcs = exposure.getWcs() 

862 photoCalib = exposure.getPhotoCalib() 

863 

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

865 

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

867 ra = row['ra'] 

868 dec = row['dec'] 

869 skyCoord = SpherePoint(ra, dec, radians) 

870 xy = wcs.skyToPixel(skyCoord) 

871 

872 try: 

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

874 except LogicError: 

875 continue 

876 

877 imFile = row[band+"imFilename"] 

878 try: 

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

880 except AttributeError: 

881 pass 

882 imFile = imFile.strip() 

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

884 

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

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

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

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

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

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

891 # exception. 

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

893 raise RuntimeError( 

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

895 ) 

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

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

898 # position. 

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

900 mat = linWcs.getMatrix() 

901 im.wcs = galsim.JacobianWCS( 

902 mat[0, 0], mat[0, 1], mat[1, 0], mat[1, 1] 

903 ) 

904 else: 

905 raise ValueError( 

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

907 ) 

908 

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

910 obj = obj.withFlux(flux) 

911 yield skyCoord, obj 

912 

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

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

915 

916 Parameters 

917 ---------- 

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

919 The catalog of fake sources to be input 

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

921 WCS to use to add fake sources 

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

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

924 The PSF information to use to make the PSF images 

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

926 Photometric calibration to be used to calibrate the fake sources 

927 band : `str` 

928 The filter band that the observation was taken in. 

929 pixelScale : `float` 

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

931 

932 Returns 

933 ------- 

934 galImages : `list` 

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

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

937 For sources labelled as galaxy. 

938 starImages : `list` 

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

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

941 For sources labelled as star. 

942 

943 Notes 

944 ----- 

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

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

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

948 """ 

949 galImages = [] 

950 starImages = [] 

951 

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

953 

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

955 fakeCat["sourceType"].array, 

956 fakeCat['mag'].array, 

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

958 

959 im = afwImage.ImageF.readFits(imFile) 

960 

961 xy = geom.Point2D(x, y) 

962 

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

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

965 try: 

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

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

968 psfKernel /= correctedFlux 

969 

970 except InvalidParameterError: 

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

972 continue 

973 

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

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

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

977 

978 try: 

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

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

981 continue 

982 

983 imSum = np.sum(outIm) 

984 divIm = outIm/imSum 

985 

986 try: 

987 flux = photoCalib.magnitudeToInstFlux(mag, xy) 

988 except LogicError: 

989 flux = 0 

990 

991 imWithFlux = flux*divIm 

992 

993 if sourceType == b"galaxy": 

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

995 if sourceType == b"star": 

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

997 

998 return galImages, starImages 

999 

1000 def addPixCoords(self, fakeCat, image): 

1001 

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

1003 

1004 Parameters 

1005 ---------- 

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

1007 The catalog of fake sources to be input 

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

1009 The image into which the fake sources should be added 

1010 

1011 Returns 

1012 ------- 

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

1014 """ 

1015 wcs = image.getWcs() 

1016 ras = fakeCat['ra'].values 

1017 decs = fakeCat['dec'].values 

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

1019 fakeCat["x"] = xs 

1020 fakeCat["y"] = ys 

1021 

1022 return fakeCat 

1023 

1024 def trimFakeCat(self, fakeCat, image): 

1025 """Trim the fake cat to about the size of the input image. 

1026 

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

1028 

1029 Parameters 

1030 ---------- 

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

1032 The catalog of fake sources to be input 

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

1034 The image into which the fake sources should be added 

1035 

1036 Returns 

1037 ------- 

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

1039 The original fakeCat trimmed to the area of the image 

1040 """ 

1041 

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

1043 xs = fakeCat["x"].values 

1044 ys = fakeCat["y"].values 

1045 

1046 isContained = xs >= bbox.minX 

1047 isContained &= xs <= bbox.maxX 

1048 isContained &= ys >= bbox.minY 

1049 isContained &= ys <= bbox.maxY 

1050 

1051 return fakeCat[isContained] 

1052 

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

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

1055 

1056 Parameters 

1057 ---------- 

1058 band : `str` 

1059 pixelScale : `float` 

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

1061 The PSF information to use to make the PSF images 

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

1063 The catalog of fake sources to be input 

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

1065 Photometric calibration to be used to calibrate the fake sources 

1066 

1067 Yields 

1068 ------- 

1069 galImages : `generator` 

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

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

1072 

1073 Notes 

1074 ----- 

1075 

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

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

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

1079 

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

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

1082 attached to the config options. 

1083 

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

1085 """ 

1086 

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

1088 

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

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

1091 

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

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

1094 try: 

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

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

1097 psfKernel /= correctedFlux 

1098 

1099 except InvalidParameterError: 

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

1101 continue 

1102 

1103 try: 

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

1105 except LogicError: 

1106 flux = 0 

1107 

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

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

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

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

1112 

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

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

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

1116 

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

1118 gal = gal.withFlux(flux) 

1119 

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

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

1122 try: 

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

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

1125 continue 

1126 

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

1128 

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

1130 

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

1132 

1133 Parameters 

1134 ---------- 

1135 band : `str` 

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

1137 The PSF information to use to make the PSF images 

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

1139 The catalog of fake sources to be input 

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

1141 The image into which the fake sources should be added 

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

1143 Photometric calibration to be used to calibrate the fake sources 

1144 

1145 Yields 

1146 ------- 

1147 starImages : `generator` 

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

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

1150 

1151 Notes 

1152 ----- 

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

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

1155 given calibration radius used in the photometric calibration step. 

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

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

1158 """ 

1159 

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

1161 

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

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

1164 

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

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

1167 try: 

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

1169 starIm = psf.computeImage(xy) 

1170 starIm /= correctedFlux 

1171 

1172 except InvalidParameterError: 

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

1174 continue 

1175 

1176 try: 

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

1178 except LogicError: 

1179 flux = 0 

1180 

1181 starIm *= flux 

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

1183 

1184 def cleanCat(self, fakeCat, starCheckVal): 

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

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

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

1188 

1189 Parameters 

1190 ---------- 

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

1192 The catalog of fake sources to be input 

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

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

1195 

1196 Returns 

1197 ------- 

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

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

1200 """ 

1201 

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

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

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

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

1206 fakeCat = fakeCat[rowsToKeep] 

1207 

1208 minN = galsim.Sersic._minimum_n 

1209 maxN = galsim.Sersic._maximum_n 

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

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

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

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

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

1215 numRowsNotUsed, minN, maxN) 

1216 fakeCat = fakeCat[rowsWithGoodSersic] 

1217 

1218 if self.config.doSubSelectSources: 

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

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

1221 fakeCat = fakeCat[fakeCat['select']] 

1222 

1223 return fakeCat 

1224 

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

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

1227 

1228 Parameters 

1229 ---------- 

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

1231 The image into which the fake sources should be added 

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

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

1234 and the locations they are to be inserted at. 

1235 sourceType : `str` 

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

1237 

1238 Returns 

1239 ------- 

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

1241 

1242 Notes 

1243 ----- 

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

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

1246 """ 

1247 

1248 imageBBox = image.getBBox() 

1249 imageMI = image.maskedImage 

1250 

1251 for (fakeImage, xy) in fakeImages: 

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

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

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

1255 if sourceType == "galaxy": 

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

1257 else: 

1258 interpFakeImage = fakeImage 

1259 

1260 interpFakeImBBox = interpFakeImage.getBBox() 

1261 interpFakeImBBox.clip(imageBBox) 

1262 

1263 if interpFakeImBBox.getArea() > 0: 

1264 imageMIView = imageMI[interpFakeImBBox] 

1265 clippedFakeImage = interpFakeImage[interpFakeImBBox] 

1266 clippedFakeImageMI = afwImage.MaskedImageF(clippedFakeImage) 

1267 clippedFakeImageMI.mask.set(self.bitmask) 

1268 imageMIView += clippedFakeImageMI 

1269 

1270 return image 

1271 

1272 def _getMetadataName(self): 

1273 """Disable metadata writing""" 

1274 return None