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

405 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-10-20 10:28 +0000

1# This file is part of pipe_tasks. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21 

22""" 

23Insert fakes into deepCoadds 

24""" 

25 

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

27 

28import galsim 

29import numpy as np 

30from astropy import units as u 

31 

32import lsst.geom as geom 

33import lsst.afw.image as afwImage 

34import lsst.afw.math as afwMath 

35import lsst.pex.config as pexConfig 

36import lsst.pipe.base as pipeBase 

37 

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

39import lsst.pipe.base.connectionTypes as cT 

40from lsst.pex.exceptions import LogicError, InvalidParameterError 

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

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

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

397 dtype=str, 

398 default="trail_length", 

399 ) 

400 

401 angle_col = pexConfig.Field( 

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

403 dtype=str, 

404 default="trail_angle", 

405 ) 

406 

407 # Deprecated config variables 

408 

409 raColName = pexConfig.Field( 

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

411 dtype=str, 

412 default="raJ2000", 

413 deprecated="Use `ra_col` instead." 

414 ) 

415 

416 decColName = pexConfig.Field( 

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

418 dtype=str, 

419 default="decJ2000", 

420 deprecated="Use `dec_col` instead." 

421 ) 

422 

423 diskHLR = pexConfig.Field( 

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

425 dtype=str, 

426 default="DiskHalfLightRadius", 

427 deprecated=( 

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

429 " to specify disk half-light ellipse." 

430 ) 

431 ) 

432 

433 aDisk = pexConfig.Field( 

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

435 "catalog.", 

436 dtype=str, 

437 default="a_d", 

438 deprecated=( 

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

440 " to specify disk half-light ellipse." 

441 ) 

442 ) 

443 

444 bDisk = pexConfig.Field( 

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

446 dtype=str, 

447 default="b_d", 

448 deprecated=( 

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

450 " to specify disk half-light ellipse." 

451 ) 

452 ) 

453 

454 paDisk = pexConfig.Field( 

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

456 dtype=str, 

457 default="pa_disk", 

458 deprecated=( 

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

460 " to specify disk half-light ellipse." 

461 ) 

462 ) 

463 

464 nDisk = pexConfig.Field( 

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

466 dtype=str, 

467 default="disk_n", 

468 deprecated="Use `disk_n_col` instead." 

469 ) 

470 

471 bulgeHLR = pexConfig.Field( 

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

473 dtype=str, 

474 default="BulgeHalfLightRadius", 

475 deprecated=( 

476 "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and " 

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

478 ) 

479 ) 

480 

481 aBulge = pexConfig.Field( 

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

483 dtype=str, 

484 default="a_b", 

485 deprecated=( 

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

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

488 ) 

489 ) 

490 

491 bBulge = pexConfig.Field( 

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

493 "catalog.", 

494 dtype=str, 

495 default="b_b", 

496 deprecated=( 

497 "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and " 

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

499 ) 

500 ) 

501 

502 paBulge = pexConfig.Field( 

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

504 dtype=str, 

505 default="pa_bulge", 

506 deprecated=( 

507 "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and " 

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

509 ) 

510 ) 

511 

512 nBulge = pexConfig.Field( 

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

514 dtype=str, 

515 default="bulge_n", 

516 deprecated="Use `bulge_n_col` instead." 

517 ) 

518 

519 magVar = pexConfig.Field( 

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

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

522 dtype=str, 

523 default="%smagVar", 

524 deprecated="Use `mag_col` instead." 

525 ) 

526 

527 sourceSelectionColName = pexConfig.Field( 

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

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

530 dtype=str, 

531 default="templateSource", 

532 deprecated="Use `select_col` instead." 

533 ) 

534 

535 

536class InsertFakesTask(PipelineTask): 

537 """Insert fake objects into images. 

538 

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

540 from the specified file and then modelled using galsim. 

541 

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

543 image. 

544 

545 `addPixCoords` 

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

547 `mkFakeGalsimGalaxies` 

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

549 `mkFakeStars` 

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

551 input file. 

552 `cleanCat` 

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

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

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

556 to only those which are True in this column. 

557 `addFakeSources` 

558 Add the fake sources to the image. 

559 

560 """ 

561 

562 _DefaultName = "insertFakes" 

563 ConfigClass = InsertFakesConfig 

564 

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

566 inputs = butlerQC.get(inputRefs) 

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

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

569 

570 outputs = self.run(**inputs) 

571 butlerQC.put(outputs, outputRefs) 

572 

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

574 """Add fake sources to an image. 

575 

576 Parameters 

577 ---------- 

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

579 The catalog of fake sources to be input 

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

581 The image into which the fake sources should be added 

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

583 WCS to use to add fake sources 

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

585 Photometric calibration to be used to calibrate the fake sources 

586 

587 Returns 

588 ------- 

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

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

591 

592 Notes 

593 ----- 

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

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

596 

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

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

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

600 the image and the image with fakes included returned. 

601 

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

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

604 """ 

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

606 # so we can reset at the end. 

607 origWcs = image.getWcs() 

608 origPhotoCalib = image.getPhotoCalib() 

609 image.setWcs(wcs) 

610 image.setPhotoCalib(photoCalib) 

611 

612 band = image.getFilter().bandLabel 

613 fakeCat = self._standardizeColumns(fakeCat, band) 

614 

615 fakeCat = self.addPixCoords(fakeCat, image) 

616 fakeCat = self.trimFakeCat(fakeCat, image) 

617 

618 if len(fakeCat) > 0: 

619 if not self.config.insertImages: 

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

621 galCheckVal = "galaxy" 

622 starCheckVal = "star" 

623 trailCheckVal = "trail" 

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

625 galCheckVal = b"galaxy" 

626 starCheckVal = b"star" 

627 trailCheckVal = b"trail" 

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

629 galCheckVal = 1 

630 starCheckVal = 0 

631 trailCheckVal = 2 

632 else: 

633 raise TypeError( 

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

635 ) 

636 if self.config.doCleanCat: 

637 fakeCat = self.cleanCat(fakeCat, starCheckVal) 

638 

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

640 trailCheckVal) 

641 else: 

642 generator = self._generateGSObjectsFromImages(image, fakeCat) 

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

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

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

646 image.mask.addMaskPlane("FAKE") 

647 else: 

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

649 

650 # restore original exposure WCS and photoCalib 

651 image.setWcs(origWcs) 

652 image.setPhotoCalib(origPhotoCalib) 

653 

654 resultStruct = pipeBase.Struct(imageWithFakes=image) 

655 

656 return resultStruct 

657 

658 def _standardizeColumns(self, fakeCat, band): 

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

660 names in the input catalog. 

661 

662 Parameters 

663 ---------- 

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

665 The catalog of fake sources to be input 

666 band : `str` 

667 Label for the current band being processed. 

668 

669 Returns 

670 ------- 

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

672 The standardized catalog of fake sources 

673 """ 

674 cfg = self.config 

675 replace_dict = {} 

676 

677 def add_to_replace_dict(new_name, depr_name, std_name): 

678 if new_name in fakeCat.columns: 

679 replace_dict[new_name] = std_name 

680 elif depr_name in fakeCat.columns: 

681 replace_dict[depr_name] = std_name 

682 else: 

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

684 

685 # Prefer new config variables over deprecated config variables. 

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

687 for new_name, depr_name, std_name in [ 

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

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

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

691 ]: 

692 add_to_replace_dict(new_name, depr_name, std_name) 

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

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

695 for new_name, depr_name, std_name in [ 

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

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

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

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

700 ]: 

701 add_to_replace_dict(new_name, depr_name, std_name) 

702 

703 if cfg.doSubSelectSources: 

704 add_to_replace_dict( 

705 cfg.select_col, 

706 cfg.sourceSelectionColName, 

707 'select' 

708 ) 

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

710 

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

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

713 # Just handle these manually. 

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

715 if ( 

716 cfg.bulge_semimajor_col in fakeCat.columns 

717 and cfg.bulge_axis_ratio_col in fakeCat.columns 

718 ): 

719 fakeCat = fakeCat.rename( 

720 columns={ 

721 cfg.bulge_semimajor_col: 'bulge_semimajor', 

722 cfg.bulge_axis_ratio_col: 'bulge_axis_ratio', 

723 cfg.disk_semimajor_col: 'disk_semimajor', 

724 cfg.disk_axis_ratio_col: 'disk_axis_ratio', 

725 }, 

726 copy=False 

727 ) 

728 elif ( 

729 cfg.bulgeHLR in fakeCat.columns 

730 and cfg.aBulge in fakeCat.columns 

731 and cfg.bBulge in fakeCat.columns 

732 ): 

733 fakeCat['bulge_axis_ratio'] = ( 

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

735 ) 

736 fakeCat['bulge_semimajor'] = ( 

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

738 ) 

739 fakeCat['disk_axis_ratio'] = ( 

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

741 ) 

742 fakeCat['disk_semimajor'] = ( 

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

744 ) 

745 else: 

746 raise ValueError( 

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

748 "axis ratio." 

749 ) 

750 

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

752 if cfg.bulge_disk_flux_ratio_col in fakeCat.columns: 

753 fakeCat = fakeCat.rename( 

754 columns={ 

755 cfg.bulge_disk_flux_ratio_col: 'bulge_disk_flux_ratio' 

756 }, 

757 copy=False 

758 ) 

759 else: 

760 fakeCat['bulge_disk_flux_ratio'] = 1.0 

761 

762 return fakeCat 

763 

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

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

766 

767 Parameters 

768 ---------- 

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

770 The exposure into which the fake sources should be added 

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

772 The catalog of fake sources to be input 

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

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

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

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

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

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

779 

780 Yields 

781 ------ 

782 gsObjects : `generator` 

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

784 """ 

785 wcs = exposure.getWcs() 

786 photoCalib = exposure.getPhotoCalib() 

787 

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

789 

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

791 ra = row['ra'] 

792 dec = row['dec'] 

793 skyCoord = SpherePoint(ra, dec, radians) 

794 xy = wcs.skyToPixel(skyCoord) 

795 

796 try: 

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

798 except LogicError: 

799 continue 

800 

801 sourceType = row[self.config.sourceType] 

802 if sourceType == galCheckVal: 

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

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

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

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

807 

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

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

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

811 

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

813 gal = gal.withFlux(flux) 

814 

815 yield skyCoord, gal 

816 elif sourceType == starCheckVal: 

817 star = galsim.DeltaFunction() 

818 star = star.withFlux(flux) 

819 yield skyCoord, star 

820 elif sourceType == trailCheckVal: 

821 length = row['trail_length'] 

822 angle = row['trail_angle'] 

823 

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

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

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

827 trail = galsim.Box(length, thickness) 

828 trail = trail.rotate(theta) 

829 trail = trail.withFlux(flux*length) 

830 

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

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

833 # coordinates, we must transform the trail here. 

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

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

836 

837 yield skyCoord, trail 

838 else: 

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

840 

841 def _generateGSObjectsFromImages(self, exposure, fakeCat): 

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

843 

844 Parameters 

845 ---------- 

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

847 The exposure into which the fake sources should be added 

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

849 The catalog of fake sources to be input 

850 

851 Yields 

852 ------ 

853 gsObjects : `generator` 

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

855 """ 

856 band = exposure.getFilter().bandLabel 

857 wcs = exposure.getWcs() 

858 photoCalib = exposure.getPhotoCalib() 

859 

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

861 

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

863 ra = row['ra'] 

864 dec = row['dec'] 

865 skyCoord = SpherePoint(ra, dec, radians) 

866 xy = wcs.skyToPixel(skyCoord) 

867 

868 try: 

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

870 except LogicError: 

871 continue 

872 

873 imFile = row[band+"imFilename"] 

874 try: 

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

876 except AttributeError: 

877 pass 

878 imFile = imFile.strip() 

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

880 

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

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

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

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

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

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

887 # exception. 

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

889 raise RuntimeError( 

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

891 ) 

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

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

894 # position. 

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

896 mat = linWcs.getMatrix() 

897 im.wcs = galsim.JacobianWCS( 

898 mat[0, 0], mat[0, 1], mat[1, 0], mat[1, 1] 

899 ) 

900 else: 

901 raise ValueError( 

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

903 ) 

904 

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

906 obj = obj.withFlux(flux) 

907 yield skyCoord, obj 

908 

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

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

911 

912 Parameters 

913 ---------- 

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

915 The catalog of fake sources to be input 

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

917 WCS to use to add fake sources 

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

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

920 The PSF information to use to make the PSF images 

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

922 Photometric calibration to be used to calibrate the fake sources 

923 band : `str` 

924 The filter band that the observation was taken in. 

925 pixelScale : `float` 

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

927 

928 Returns 

929 ------- 

930 galImages : `list` 

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

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

933 For sources labelled as galaxy. 

934 starImages : `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 star. 

938 

939 Notes 

940 ----- 

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

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

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

944 """ 

945 galImages = [] 

946 starImages = [] 

947 

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

949 

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

951 fakeCat["sourceType"].array, 

952 fakeCat['mag'].array, 

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

954 

955 im = afwImage.ImageF.readFits(imFile) 

956 

957 xy = geom.Point2D(x, y) 

958 

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

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

961 try: 

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

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

964 psfKernel /= correctedFlux 

965 

966 except InvalidParameterError: 

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

968 continue 

969 

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

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

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

973 

974 try: 

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

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

977 continue 

978 

979 imSum = np.sum(outIm) 

980 divIm = outIm/imSum 

981 

982 try: 

983 flux = photoCalib.magnitudeToInstFlux(mag, xy) 

984 except LogicError: 

985 flux = 0 

986 

987 imWithFlux = flux*divIm 

988 

989 if sourceType == b"galaxy": 

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

991 if sourceType == b"star": 

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

993 

994 return galImages, starImages 

995 

996 def addPixCoords(self, fakeCat, image): 

997 

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

999 

1000 Parameters 

1001 ---------- 

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

1003 The catalog of fake sources to be input 

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

1005 The image into which the fake sources should be added 

1006 

1007 Returns 

1008 ------- 

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

1010 """ 

1011 wcs = image.getWcs() 

1012 ras = fakeCat['ra'].values 

1013 decs = fakeCat['dec'].values 

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

1015 fakeCat["x"] = xs 

1016 fakeCat["y"] = ys 

1017 

1018 return fakeCat 

1019 

1020 def trimFakeCat(self, fakeCat, image): 

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

1022 

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

1024 

1025 Parameters 

1026 ---------- 

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

1028 The catalog of fake sources to be input 

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

1030 The image into which the fake sources should be added 

1031 

1032 Returns 

1033 ------- 

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

1035 The original fakeCat trimmed to the area of the image 

1036 """ 

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

1038 

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

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

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

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

1043 

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

1045 

1046 # also filter on the image BBox in pixel space 

1047 xs = fakeCat["x"].values 

1048 ys = fakeCat["y"].values 

1049 

1050 isContainedXy = xs >= wideBbox.minX 

1051 isContainedXy &= xs <= wideBbox.maxX 

1052 isContainedXy &= ys >= wideBbox.minY 

1053 isContainedXy &= ys <= wideBbox.maxY 

1054 

1055 return fakeCat[isContainedRaDec & isContainedXy] 

1056 

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

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

1059 

1060 Parameters 

1061 ---------- 

1062 band : `str` 

1063 pixelScale : `float` 

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

1065 The PSF information to use to make the PSF images 

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

1067 The catalog of fake sources to be input 

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

1069 Photometric calibration to be used to calibrate the fake sources 

1070 

1071 Yields 

1072 ------ 

1073 galImages : `generator` 

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

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

1076 

1077 Notes 

1078 ----- 

1079 

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

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

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

1083 

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

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

1086 attached to the config options. 

1087 

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

1089 """ 

1090 

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

1092 

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

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

1095 

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

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

1098 try: 

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

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

1101 psfKernel /= correctedFlux 

1102 

1103 except InvalidParameterError: 

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

1105 continue 

1106 

1107 try: 

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

1109 except LogicError: 

1110 flux = 0 

1111 

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

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

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

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

1116 

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

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

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

1120 

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

1122 gal = gal.withFlux(flux) 

1123 

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

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

1126 try: 

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

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

1129 continue 

1130 

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

1132 

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

1134 

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

1136 

1137 Parameters 

1138 ---------- 

1139 band : `str` 

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

1141 The PSF information to use to make the PSF images 

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

1143 The catalog of fake sources to be input 

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

1145 The image into which the fake sources should be added 

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

1147 Photometric calibration to be used to calibrate the fake sources 

1148 

1149 Yields 

1150 ------ 

1151 starImages : `generator` 

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

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

1154 

1155 Notes 

1156 ----- 

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

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

1159 given calibration radius used in the photometric calibration step. 

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

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

1162 """ 

1163 

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

1165 

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

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

1168 

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

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

1171 try: 

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

1173 starIm = psf.computeImage(xy) 

1174 starIm /= correctedFlux 

1175 

1176 except InvalidParameterError: 

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

1178 continue 

1179 

1180 try: 

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

1182 except LogicError: 

1183 flux = 0 

1184 

1185 starIm *= flux 

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

1187 

1188 def cleanCat(self, fakeCat, starCheckVal): 

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

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

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

1192 

1193 Parameters 

1194 ---------- 

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

1196 The catalog of fake sources to be input 

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

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

1199 

1200 Returns 

1201 ------- 

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

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

1204 """ 

1205 

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

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

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

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

1210 fakeCat = fakeCat[rowsToKeep] 

1211 

1212 minN = galsim.Sersic._minimum_n 

1213 maxN = galsim.Sersic._maximum_n 

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

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

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

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

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

1219 numRowsNotUsed, minN, maxN) 

1220 fakeCat = fakeCat[rowsWithGoodSersic] 

1221 

1222 if self.config.doSubSelectSources: 

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

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

1225 fakeCat = fakeCat[fakeCat['select']] 

1226 

1227 return fakeCat 

1228 

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

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

1231 

1232 Parameters 

1233 ---------- 

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

1235 The image into which the fake sources should be added 

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

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

1238 and the locations they are to be inserted at. 

1239 sourceType : `str` 

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

1241 

1242 Returns 

1243 ------- 

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

1245 

1246 Notes 

1247 ----- 

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

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

1250 """ 

1251 

1252 imageBBox = image.getBBox() 

1253 imageMI = image.maskedImage 

1254 

1255 for (fakeImage, xy) in fakeImages: 

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

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

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

1259 if sourceType == "galaxy": 

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

1261 else: 

1262 interpFakeImage = fakeImage 

1263 

1264 interpFakeImBBox = interpFakeImage.getBBox() 

1265 interpFakeImBBox.clip(imageBBox) 

1266 

1267 if interpFakeImBBox.getArea() > 0: 

1268 imageMIView = imageMI[interpFakeImBBox] 

1269 clippedFakeImage = interpFakeImage[interpFakeImBBox] 

1270 clippedFakeImageMI = afwImage.MaskedImageF(clippedFakeImage) 

1271 clippedFakeImageMI.mask.set(self.bitmask) 

1272 imageMIView += clippedFakeImageMI 

1273 

1274 return image