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

405 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2022-09-12 01:26 -0700

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 

26import numpy as np 

27from astropy import units as u 

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 PipelineTask, PipelineTaskConfig, PipelineTaskConnections 

36import lsst.pipe.base.connectionTypes as cT 

37from lsst.pex.exceptions import LogicError, InvalidParameterError 

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

39 

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

41 

42 

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

44 """Add fake sources to the given exposure 

45 

46 Parameters 

47 ---------- 

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

49 The exposure into which the fake sources should be added 

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

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

52 surface brightness profiles to inject. 

53 calibFluxRadius : `float`, optional 

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

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

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

57 slot_CalibFlux_instFlux. 

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

59 Logger. 

60 """ 

61 exposure.mask.addMaskPlane("FAKE") 

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

63 if logger: 

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

65 

66 wcs = exposure.getWcs() 

67 psf = exposure.getPsf() 

68 

69 bbox = exposure.getBBox() 

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

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

72 

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

74 

75 for spt, gsObj in objects: 

76 pt = wcs.skyToPixel(spt) 

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

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

79 if logger: 

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

81 

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

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

84 

85 # This check is here because sometimes the WCS 

86 # is multivalued and objects that should not be 

87 # were being included. 

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

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

90 continue 

91 

92 try: 

93 psfArr = psf.computeKernelImage(pt).array 

94 except InvalidParameterError: 

95 # Try mapping to nearest point contained in bbox. 

96 contained_pt = Point2D( 

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

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

99 ) 

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

101 if logger: 

102 logger.infof( 

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

104 pt 

105 ) 

106 continue 

107 # otherwise, try again with new point 

108 try: 

109 psfArr = psf.computeKernelImage(contained_pt).array 

110 except InvalidParameterError: 

111 if logger: 

112 logger.infof( 

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

114 pt 

115 ) 

116 continue 

117 

118 apCorr = psf.computeApertureFlux(calibFluxRadius) 

119 psfArr /= apCorr 

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

121 

122 conv = galsim.Convolve(gsObj, gsPSF) 

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

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

125 subBounds &= fullBounds 

126 

127 if subBounds.area() > 0: 

128 subImg = gsImg[subBounds] 

129 offset = posd - subBounds.true_center 

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

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

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

133 

134 conv.drawImage( 

135 subImg, 

136 add_to_image=True, 

137 offset=offset, 

138 wcs=gsWCS, 

139 method='no_pixel' 

140 ) 

141 

142 subBox = geom.Box2I( 

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

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

145 ) 

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

147 

148 

149def _isWCSGalsimDefault(wcs, hdr): 

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

151 or if it's just the galsim default. 

152 

153 Parameters 

154 ---------- 

155 wcs : galsim.BaseWCS 

156 Potentially default WCS. 

157 hdr : galsim.fits.FitsHeader 

158 Header as read in by galsim. 

159 

160 Returns 

161 ------- 

162 isDefault : bool 

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

164 """ 

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

166 return False 

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

168 return False 

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

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

171 for wcs_type in galsim.fitswcs.fits_wcs_types: 

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

173 try: 

174 wcs_type._readHeader(hdr) 

175 return False 

176 except Exception: 

177 pass 

178 else: 

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

180 

181 

182class InsertFakesConnections(PipelineTaskConnections, 

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

184 "fakesType": "fakes_"}, 

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

186 

187 image = cT.Input( 

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

189 name="{coaddName}Coadd", 

190 storageClass="ExposureF", 

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

192 ) 

193 

194 fakeCat = cT.Input( 

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

196 name="{fakesType}fakeSourceCat", 

197 storageClass="DataFrame", 

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

199 ) 

200 

201 imageWithFakes = cT.Output( 

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

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

204 storageClass="ExposureF", 

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

206 ) 

207 

208 

209class InsertFakesConfig(PipelineTaskConfig, 

210 pipelineConnections=InsertFakesConnections): 

211 """Config for inserting fake sources 

212 """ 

213 

214 # Unchanged 

215 

216 doCleanCat = pexConfig.Field( 

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

218 dtype=bool, 

219 default=True, 

220 ) 

221 

222 fakeType = pexConfig.Field( 

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

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

225 "catalog.", 

226 dtype=str, 

227 default="static", 

228 ) 

229 

230 calibFluxRadius = pexConfig.Field( 

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

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

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

234 dtype=float, 

235 default=12.0, 

236 ) 

237 

238 coaddName = pexConfig.Field( 

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

240 dtype=str, 

241 default="deep", 

242 ) 

243 

244 doSubSelectSources = pexConfig.Field( 

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

246 "set in the sourceSelectionColName config option.", 

247 dtype=bool, 

248 default=False 

249 ) 

250 

251 insertImages = pexConfig.Field( 

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

253 dtype=bool, 

254 default=False, 

255 ) 

256 

257 insertOnlyStars = pexConfig.Field( 

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

259 dtype=bool, 

260 default=False, 

261 ) 

262 

263 doProcessAllDataIds = pexConfig.Field( 

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

265 dtype=bool, 

266 default=False, 

267 ) 

268 

269 trimBuffer = pexConfig.Field( 

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

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

272 dtype=int, 

273 default=100, 

274 ) 

275 

276 sourceType = pexConfig.Field( 

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

278 dtype=str, 

279 default="sourceType", 

280 ) 

281 

282 fits_alignment = pexConfig.ChoiceField( 

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

284 dtype=str, 

285 allowed={ 

286 "wcs": ( 

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

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

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

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

291 "image." 

292 ), 

293 "pixel": ( 

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

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

296 "distances in the target image." 

297 ) 

298 }, 

299 default="pixel" 

300 ) 

301 

302 # New source catalog config variables 

303 

304 ra_col = pexConfig.Field( 

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

306 dtype=str, 

307 default="ra", 

308 ) 

309 

310 dec_col = pexConfig.Field( 

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

312 dtype=str, 

313 default="dec", 

314 ) 

315 

316 bulge_semimajor_col = pexConfig.Field( 

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

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

319 dtype=str, 

320 default="bulge_semimajor", 

321 ) 

322 

323 bulge_axis_ratio_col = pexConfig.Field( 

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

325 "half-light ellipse.", 

326 dtype=str, 

327 default="bulge_axis_ratio", 

328 ) 

329 

330 bulge_pa_col = pexConfig.Field( 

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

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

333 "half-light ellipse.", 

334 dtype=str, 

335 default="bulge_pa", 

336 ) 

337 

338 bulge_n_col = pexConfig.Field( 

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

340 dtype=str, 

341 default="bulge_n", 

342 ) 

343 

344 disk_semimajor_col = pexConfig.Field( 

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

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

347 dtype=str, 

348 default="disk_semimajor", 

349 ) 

350 

351 disk_axis_ratio_col = pexConfig.Field( 

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

353 "half-light ellipse.", 

354 dtype=str, 

355 default="disk_axis_ratio", 

356 ) 

357 

358 disk_pa_col = pexConfig.Field( 

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

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

361 "half-light ellipse.", 

362 dtype=str, 

363 default="disk_pa", 

364 ) 

365 

366 disk_n_col = pexConfig.Field( 

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

368 dtype=str, 

369 default="disk_n", 

370 ) 

371 

372 bulge_disk_flux_ratio_col = pexConfig.Field( 

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

374 dtype=str, 

375 default="bulge_disk_flux_ratio", 

376 ) 

377 

378 mag_col = pexConfig.Field( 

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

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

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

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

383 dtype=str, 

384 default="%s_mag" 

385 ) 

386 

387 select_col = pexConfig.Field( 

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

389 "add.", 

390 dtype=str, 

391 default="select", 

392 ) 

393 

394 length_col = pexConfig.Field( 

395 doc="Source catalog column name for trail length (in pixels).", 

396 dtype=str, 

397 default="trail_length", 

398 ) 

399 

400 angle_col = pexConfig.Field( 

401 doc="Source catalog column name for trail angle (in radians).", 

402 dtype=str, 

403 default="trail_angle", 

404 ) 

405 

406 # Deprecated config variables 

407 

408 raColName = pexConfig.Field( 

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

410 dtype=str, 

411 default="raJ2000", 

412 deprecated="Use `ra_col` instead." 

413 ) 

414 

415 decColName = pexConfig.Field( 

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

417 dtype=str, 

418 default="decJ2000", 

419 deprecated="Use `dec_col` instead." 

420 ) 

421 

422 diskHLR = pexConfig.Field( 

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

424 dtype=str, 

425 default="DiskHalfLightRadius", 

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 aDisk = pexConfig.Field( 

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

434 "catalog.", 

435 dtype=str, 

436 default="a_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 bDisk = pexConfig.Field( 

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

445 dtype=str, 

446 default="b_d", 

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 paDisk = pexConfig.Field( 

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

455 dtype=str, 

456 default="pa_disk", 

457 deprecated=( 

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

459 " to specify disk half-light ellipse." 

460 ) 

461 ) 

462 

463 nDisk = pexConfig.Field( 

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

465 dtype=str, 

466 default="disk_n", 

467 deprecated="Use `disk_n_col` instead." 

468 ) 

469 

470 bulgeHLR = pexConfig.Field( 

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

472 dtype=str, 

473 default="BulgeHalfLightRadius", 

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 aBulge = pexConfig.Field( 

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

482 dtype=str, 

483 default="a_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 bBulge = pexConfig.Field( 

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

492 "catalog.", 

493 dtype=str, 

494 default="b_b", 

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 paBulge = pexConfig.Field( 

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

503 dtype=str, 

504 default="pa_bulge", 

505 deprecated=( 

506 "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and " 

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

508 ) 

509 ) 

510 

511 nBulge = pexConfig.Field( 

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

513 dtype=str, 

514 default="bulge_n", 

515 deprecated="Use `bulge_n_col` instead." 

516 ) 

517 

518 magVar = pexConfig.Field( 

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

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

521 dtype=str, 

522 default="%smagVar", 

523 deprecated="Use `mag_col` instead." 

524 ) 

525 

526 sourceSelectionColName = pexConfig.Field( 

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

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

529 dtype=str, 

530 default="templateSource", 

531 deprecated="Use `select_col` instead." 

532 ) 

533 

534 

535class InsertFakesTask(PipelineTask): 

536 """Insert fake objects into images. 

537 

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

539 from the specified file and then modelled using galsim. 

540 

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

542 image. 

543 

544 `addPixCoords` 

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

546 `mkFakeGalsimGalaxies` 

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

548 `mkFakeStars` 

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

550 input file. 

551 `cleanCat` 

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

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

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

555 to only those which are True in this column. 

556 `addFakeSources` 

557 Add the fake sources to the image. 

558 

559 """ 

560 

561 _DefaultName = "insertFakes" 

562 ConfigClass = InsertFakesConfig 

563 

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

565 inputs = butlerQC.get(inputRefs) 

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

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

568 

569 outputs = self.run(**inputs) 

570 butlerQC.put(outputs, outputRefs) 

571 

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

573 """Add fake sources to an image. 

574 

575 Parameters 

576 ---------- 

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

578 The catalog of fake sources to be input 

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

580 The image into which the fake sources should be added 

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

582 WCS to use to add fake sources 

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

584 Photometric calibration to be used to calibrate the fake sources 

585 

586 Returns 

587 ------- 

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

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

590 

591 Notes 

592 ----- 

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

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

595 

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

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

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

599 the image and the image with fakes included returned. 

600 

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

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

603 """ 

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

605 # so we can reset at the end. 

606 origWcs = image.getWcs() 

607 origPhotoCalib = image.getPhotoCalib() 

608 image.setWcs(wcs) 

609 image.setPhotoCalib(photoCalib) 

610 

611 band = image.getFilter().bandLabel 

612 fakeCat = self._standardizeColumns(fakeCat, band) 

613 

614 fakeCat = self.addPixCoords(fakeCat, image) 

615 fakeCat = self.trimFakeCat(fakeCat, image) 

616 

617 if len(fakeCat) > 0: 

618 if not self.config.insertImages: 

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

620 galCheckVal = "galaxy" 

621 starCheckVal = "star" 

622 trailCheckVal = "trail" 

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

624 galCheckVal = b"galaxy" 

625 starCheckVal = b"star" 

626 trailCheckVal = b"trail" 

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

628 galCheckVal = 1 

629 starCheckVal = 0 

630 trailCheckVal = 2 

631 else: 

632 raise TypeError( 

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

634 ) 

635 if self.config.doCleanCat: 

636 fakeCat = self.cleanCat(fakeCat, starCheckVal) 

637 

638 generator = self._generateGSObjectsFromCatalog(image, fakeCat, galCheckVal, starCheckVal, 

639 trailCheckVal) 

640 else: 

641 generator = self._generateGSObjectsFromImages(image, fakeCat) 

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

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

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

645 image.mask.addMaskPlane("FAKE") 

646 else: 

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

648 

649 # restore original exposure WCS and photoCalib 

650 image.setWcs(origWcs) 

651 image.setPhotoCalib(origPhotoCalib) 

652 

653 resultStruct = pipeBase.Struct(imageWithFakes=image) 

654 

655 return resultStruct 

656 

657 def _standardizeColumns(self, fakeCat, band): 

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

659 names in the input catalog. 

660 

661 Parameters 

662 ---------- 

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

664 The catalog of fake sources to be input 

665 band : `str` 

666 Label for the current band being processed. 

667 

668 Returns 

669 ------- 

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

671 The standardized catalog of fake sources 

672 """ 

673 cfg = self.config 

674 replace_dict = {} 

675 

676 def add_to_replace_dict(new_name, depr_name, std_name): 

677 if new_name in fakeCat.columns: 

678 replace_dict[new_name] = std_name 

679 elif depr_name in fakeCat.columns: 

680 replace_dict[depr_name] = std_name 

681 else: 

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

683 

684 # Prefer new config variables over deprecated config variables. 

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

686 for new_name, depr_name, std_name in [ 

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

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

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

690 ]: 

691 add_to_replace_dict(new_name, depr_name, std_name) 

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

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

694 for new_name, depr_name, std_name in [ 

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

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

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

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

699 ]: 

700 add_to_replace_dict(new_name, depr_name, std_name) 

701 

702 if cfg.doSubSelectSources: 

703 add_to_replace_dict( 

704 cfg.select_col, 

705 cfg.sourceSelectionColName, 

706 'select' 

707 ) 

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

709 

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

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

712 # Just handle these manually. 

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

714 if ( 

715 cfg.bulge_semimajor_col in fakeCat.columns 

716 and cfg.bulge_axis_ratio_col in fakeCat.columns 

717 ): 

718 fakeCat = fakeCat.rename( 

719 columns={ 

720 cfg.bulge_semimajor_col: 'bulge_semimajor', 

721 cfg.bulge_axis_ratio_col: 'bulge_axis_ratio', 

722 cfg.disk_semimajor_col: 'disk_semimajor', 

723 cfg.disk_axis_ratio_col: 'disk_axis_ratio', 

724 }, 

725 copy=False 

726 ) 

727 elif ( 

728 cfg.bulgeHLR in fakeCat.columns 

729 and cfg.aBulge in fakeCat.columns 

730 and cfg.bBulge in fakeCat.columns 

731 ): 

732 fakeCat['bulge_axis_ratio'] = ( 

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

734 ) 

735 fakeCat['bulge_semimajor'] = ( 

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

737 ) 

738 fakeCat['disk_axis_ratio'] = ( 

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

740 ) 

741 fakeCat['disk_semimajor'] = ( 

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

743 ) 

744 else: 

745 raise ValueError( 

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

747 "axis ratio." 

748 ) 

749 

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

751 if cfg.bulge_disk_flux_ratio_col in fakeCat.columns: 

752 fakeCat = fakeCat.rename( 

753 columns={ 

754 cfg.bulge_disk_flux_ratio_col: 'bulge_disk_flux_ratio' 

755 }, 

756 copy=False 

757 ) 

758 else: 

759 fakeCat['bulge_disk_flux_ratio'] = 1.0 

760 

761 return fakeCat 

762 

763 def _generateGSObjectsFromCatalog(self, exposure, fakeCat, galCheckVal, starCheckVal, trailCheckVal): 

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

765 

766 Parameters 

767 ---------- 

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

769 The exposure into which the fake sources should be added 

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

771 The catalog of fake sources to be input 

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

773 The value that is set in the sourceType column to specify an object is a galaxy. 

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

775 The value that is set in the sourceType column to specify an object is a star. 

776 trailCheckVal : `str`, `bytes` or `int` 

777 The value that is set in the sourceType column to specify an object is a star 

778 

779 Yields 

780 ------ 

781 gsObjects : `generator` 

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

783 """ 

784 wcs = exposure.getWcs() 

785 photoCalib = exposure.getPhotoCalib() 

786 

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

788 

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

790 ra = row['ra'] 

791 dec = row['dec'] 

792 skyCoord = SpherePoint(ra, dec, radians) 

793 xy = wcs.skyToPixel(skyCoord) 

794 

795 try: 

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

797 except LogicError: 

798 continue 

799 

800 sourceType = row[self.config.sourceType] 

801 if sourceType == galCheckVal: 

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

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

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

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

806 

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

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

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

810 

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

812 gal = gal.withFlux(flux) 

813 

814 yield skyCoord, gal 

815 elif sourceType == starCheckVal: 

816 star = galsim.DeltaFunction() 

817 star = star.withFlux(flux) 

818 yield skyCoord, star 

819 elif sourceType == trailCheckVal: 

820 length = row['trail_length'] 

821 angle = row['trail_angle'] 

822 

823 # Make a 'thin' box to mimic a line surface brightness profile 

824 thickness = 1e-6 # Make the box much thinner than a pixel 

825 theta = galsim.Angle(angle*galsim.radians) 

826 trail = galsim.Box(length, thickness) 

827 trail = trail.rotate(theta) 

828 trail = trail.withFlux(flux*length) 

829 

830 # Galsim objects are assumed to be in sky-coordinates. Since 

831 # we want the trail to appear as defined above in image- 

832 # coordinates, we must transform the trail here. 

833 mat = wcs.linearizePixelToSky(skyCoord, geom.arcseconds).getMatrix() 

834 trail = trail.transform(mat[0, 0], mat[0, 1], mat[1, 0], mat[1, 1]) 

835 

836 yield skyCoord, trail 

837 else: 

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

839 

840 def _generateGSObjectsFromImages(self, exposure, fakeCat): 

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

842 

843 Parameters 

844 ---------- 

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

846 The exposure into which the fake sources should be added 

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

848 The catalog of fake sources to be input 

849 

850 Yields 

851 ------ 

852 gsObjects : `generator` 

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

854 """ 

855 band = exposure.getFilter().bandLabel 

856 wcs = exposure.getWcs() 

857 photoCalib = exposure.getPhotoCalib() 

858 

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

860 

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

862 ra = row['ra'] 

863 dec = row['dec'] 

864 skyCoord = SpherePoint(ra, dec, radians) 

865 xy = wcs.skyToPixel(skyCoord) 

866 

867 try: 

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

869 except LogicError: 

870 continue 

871 

872 imFile = row[band+"imFilename"] 

873 try: 

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

875 except AttributeError: 

876 pass 

877 imFile = imFile.strip() 

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

879 

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

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

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

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

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

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

886 # exception. 

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

888 raise RuntimeError( 

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

890 ) 

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

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

893 # position. 

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

895 mat = linWcs.getMatrix() 

896 im.wcs = galsim.JacobianWCS( 

897 mat[0, 0], mat[0, 1], mat[1, 0], mat[1, 1] 

898 ) 

899 else: 

900 raise ValueError( 

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

902 ) 

903 

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

905 obj = obj.withFlux(flux) 

906 yield skyCoord, obj 

907 

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

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

910 

911 Parameters 

912 ---------- 

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

914 The catalog of fake sources to be input 

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

916 WCS to use to add fake sources 

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

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

919 The PSF information to use to make the PSF images 

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

921 Photometric calibration to be used to calibrate the fake sources 

922 band : `str` 

923 The filter band that the observation was taken in. 

924 pixelScale : `float` 

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

926 

927 Returns 

928 ------- 

929 galImages : `list` 

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

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

932 For sources labelled as galaxy. 

933 starImages : `list` 

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

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

936 For sources labelled as star. 

937 

938 Notes 

939 ----- 

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

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

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

943 """ 

944 galImages = [] 

945 starImages = [] 

946 

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

948 

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

950 fakeCat["sourceType"].array, 

951 fakeCat['mag'].array, 

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

953 

954 im = afwImage.ImageF.readFits(imFile) 

955 

956 xy = geom.Point2D(x, y) 

957 

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

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

960 try: 

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

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

963 psfKernel /= correctedFlux 

964 

965 except InvalidParameterError: 

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

967 continue 

968 

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

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

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

972 

973 try: 

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

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

976 continue 

977 

978 imSum = np.sum(outIm) 

979 divIm = outIm/imSum 

980 

981 try: 

982 flux = photoCalib.magnitudeToInstFlux(mag, xy) 

983 except LogicError: 

984 flux = 0 

985 

986 imWithFlux = flux*divIm 

987 

988 if sourceType == b"galaxy": 

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

990 if sourceType == b"star": 

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

992 

993 return galImages, starImages 

994 

995 def addPixCoords(self, fakeCat, image): 

996 

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

998 

999 Parameters 

1000 ---------- 

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

1002 The catalog of fake sources to be input 

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

1004 The image into which the fake sources should be added 

1005 

1006 Returns 

1007 ------- 

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

1009 """ 

1010 wcs = image.getWcs() 

1011 ras = fakeCat['ra'].values 

1012 decs = fakeCat['dec'].values 

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

1014 fakeCat["x"] = xs 

1015 fakeCat["y"] = ys 

1016 

1017 return fakeCat 

1018 

1019 def trimFakeCat(self, fakeCat, image): 

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

1021 

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

1023 

1024 Parameters 

1025 ---------- 

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

1027 The catalog of fake sources to be input 

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

1029 The image into which the fake sources should be added 

1030 

1031 Returns 

1032 ------- 

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

1034 The original fakeCat trimmed to the area of the image 

1035 """ 

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

1037 

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

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

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

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

1042 

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

1044 

1045 # also filter on the image BBox in pixel space 

1046 xs = fakeCat["x"].values 

1047 ys = fakeCat["y"].values 

1048 

1049 isContainedXy = xs >= wideBbox.minX 

1050 isContainedXy &= xs <= wideBbox.maxX 

1051 isContainedXy &= ys >= wideBbox.minY 

1052 isContainedXy &= ys <= wideBbox.maxY 

1053 

1054 return fakeCat[isContainedRaDec & isContainedXy] 

1055 

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

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

1058 

1059 Parameters 

1060 ---------- 

1061 band : `str` 

1062 pixelScale : `float` 

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

1064 The PSF information to use to make the PSF images 

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

1066 The catalog of fake sources to be input 

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

1068 Photometric calibration to be used to calibrate the fake sources 

1069 

1070 Yields 

1071 ------- 

1072 galImages : `generator` 

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

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

1075 

1076 Notes 

1077 ----- 

1078 

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

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

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

1082 

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

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

1085 attached to the config options. 

1086 

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

1088 """ 

1089 

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

1091 

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

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

1094 

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

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

1097 try: 

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

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

1100 psfKernel /= correctedFlux 

1101 

1102 except InvalidParameterError: 

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

1104 continue 

1105 

1106 try: 

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

1108 except LogicError: 

1109 flux = 0 

1110 

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

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

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

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

1115 

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

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

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

1119 

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

1121 gal = gal.withFlux(flux) 

1122 

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

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

1125 try: 

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

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

1128 continue 

1129 

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

1131 

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

1133 

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

1135 

1136 Parameters 

1137 ---------- 

1138 band : `str` 

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

1140 The PSF information to use to make the PSF images 

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

1142 The catalog of fake sources to be input 

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

1144 The image into which the fake sources should be added 

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

1146 Photometric calibration to be used to calibrate the fake sources 

1147 

1148 Yields 

1149 ------- 

1150 starImages : `generator` 

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

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

1153 

1154 Notes 

1155 ----- 

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

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

1158 given calibration radius used in the photometric calibration step. 

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

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

1161 """ 

1162 

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

1164 

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

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

1167 

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

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

1170 try: 

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

1172 starIm = psf.computeImage(xy) 

1173 starIm /= correctedFlux 

1174 

1175 except InvalidParameterError: 

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

1177 continue 

1178 

1179 try: 

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

1181 except LogicError: 

1182 flux = 0 

1183 

1184 starIm *= flux 

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

1186 

1187 def cleanCat(self, fakeCat, starCheckVal): 

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

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

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

1191 

1192 Parameters 

1193 ---------- 

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

1195 The catalog of fake sources to be input 

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

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

1198 

1199 Returns 

1200 ------- 

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

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

1203 """ 

1204 

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

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

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

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

1209 fakeCat = fakeCat[rowsToKeep] 

1210 

1211 minN = galsim.Sersic._minimum_n 

1212 maxN = galsim.Sersic._maximum_n 

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

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

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

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

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

1218 numRowsNotUsed, minN, maxN) 

1219 fakeCat = fakeCat[rowsWithGoodSersic] 

1220 

1221 if self.config.doSubSelectSources: 

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

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

1224 fakeCat = fakeCat[fakeCat['select']] 

1225 

1226 return fakeCat 

1227 

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

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

1230 

1231 Parameters 

1232 ---------- 

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

1234 The image into which the fake sources should be added 

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

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

1237 and the locations they are to be inserted at. 

1238 sourceType : `str` 

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

1240 

1241 Returns 

1242 ------- 

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

1244 

1245 Notes 

1246 ----- 

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

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

1249 """ 

1250 

1251 imageBBox = image.getBBox() 

1252 imageMI = image.maskedImage 

1253 

1254 for (fakeImage, xy) in fakeImages: 

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

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

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

1258 if sourceType == "galaxy": 

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

1260 else: 

1261 interpFakeImage = fakeImage 

1262 

1263 interpFakeImBBox = interpFakeImage.getBBox() 

1264 interpFakeImBBox.clip(imageBBox) 

1265 

1266 if interpFakeImBBox.getArea() > 0: 

1267 imageMIView = imageMI[interpFakeImBBox] 

1268 clippedFakeImage = interpFakeImage[interpFakeImBBox] 

1269 clippedFakeImageMI = afwImage.MaskedImageF(clippedFakeImage) 

1270 clippedFakeImageMI.mask.set(self.bitmask) 

1271 imageMIView += clippedFakeImageMI 

1272 

1273 return image