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

406 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-03-29 03:09 -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# (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("Adding mask plane with bitmask %s", 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("Adding fake source at %s", 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 apCorr = psf.computeApertureFlux(calibFluxRadius, pt) 

96 except InvalidParameterError: 

97 # Try mapping to nearest point contained in bbox. 

98 contained_pt = Point2D( 

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

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

101 ) 

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

103 if logger: 

104 logger.info("Cannot compute Psf for object at %s; skipping", pt) 

105 continue 

106 # otherwise, try again with new point 

107 try: 

108 psfArr = psf.computeKernelImage(contained_pt).array 

109 apCorr = psf.computeApertureFlux(calibFluxRadius, contained_pt) 

110 except InvalidParameterError: 

111 if logger: 

112 logger.info("Cannot compute Psf for object at %s; skipping", pt) 

113 continue 

114 

115 psfArr /= apCorr 

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

117 

118 conv = galsim.Convolve(gsObj, gsPSF) 

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

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

121 subBounds &= fullBounds 

122 

123 if subBounds.area() > 0: 

124 subImg = gsImg[subBounds] 

125 offset = posd - subBounds.true_center 

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

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

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

129 

130 conv.drawImage( 

131 subImg, 

132 add_to_image=True, 

133 offset=offset, 

134 wcs=gsWCS, 

135 method='no_pixel' 

136 ) 

137 

138 subBox = geom.Box2I( 

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

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

141 ) 

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

143 

144 

145def _isWCSGalsimDefault(wcs, hdr): 

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

147 or if it's just the galsim default. 

148 

149 Parameters 

150 ---------- 

151 wcs : galsim.BaseWCS 

152 Potentially default WCS. 

153 hdr : galsim.fits.FitsHeader 

154 Header as read in by galsim. 

155 

156 Returns 

157 ------- 

158 isDefault : bool 

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

160 """ 

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

162 return False 

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

164 return False 

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

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

167 for wcs_type in galsim.fitswcs.fits_wcs_types: 

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

169 try: 

170 wcs_type._readHeader(hdr) 

171 return False 

172 except Exception: 

173 pass 

174 else: 

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

176 

177 

178class InsertFakesConnections(PipelineTaskConnections, 

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

180 "fakesType": "fakes_"}, 

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

182 

183 image = cT.Input( 

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

185 name="{coaddName}Coadd", 

186 storageClass="ExposureF", 

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

188 ) 

189 

190 fakeCat = cT.Input( 

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

192 name="{fakesType}fakeSourceCat", 

193 storageClass="DataFrame", 

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

195 ) 

196 

197 imageWithFakes = cT.Output( 

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

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

200 storageClass="ExposureF", 

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

202 ) 

203 

204 

205class InsertFakesConfig(PipelineTaskConfig, 

206 pipelineConnections=InsertFakesConnections): 

207 """Config for inserting fake sources 

208 """ 

209 

210 # Unchanged 

211 

212 doCleanCat = pexConfig.Field( 

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

214 dtype=bool, 

215 default=True, 

216 ) 

217 

218 fakeType = pexConfig.Field( 

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

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

221 "catalog.", 

222 dtype=str, 

223 default="static", 

224 ) 

225 

226 calibFluxRadius = pexConfig.Field( 

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

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

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

230 dtype=float, 

231 default=12.0, 

232 ) 

233 

234 coaddName = pexConfig.Field( 

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

236 dtype=str, 

237 default="deep", 

238 ) 

239 

240 doSubSelectSources = pexConfig.Field( 

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

242 "set in the sourceSelectionColName config option.", 

243 dtype=bool, 

244 default=False 

245 ) 

246 

247 insertImages = pexConfig.Field( 

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

249 dtype=bool, 

250 default=False, 

251 ) 

252 

253 insertOnlyStars = pexConfig.Field( 

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

255 dtype=bool, 

256 default=False, 

257 ) 

258 

259 doProcessAllDataIds = pexConfig.Field( 

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

261 dtype=bool, 

262 default=False, 

263 ) 

264 

265 trimBuffer = pexConfig.Field( 

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

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

268 dtype=int, 

269 default=100, 

270 ) 

271 

272 sourceType = pexConfig.Field( 

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

274 dtype=str, 

275 default="sourceType", 

276 ) 

277 

278 fits_alignment = pexConfig.ChoiceField( 

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

280 dtype=str, 

281 allowed={ 

282 "wcs": ( 

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

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

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

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

287 "image." 

288 ), 

289 "pixel": ( 

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

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

292 "distances in the target image." 

293 ) 

294 }, 

295 default="pixel" 

296 ) 

297 

298 # New source catalog config variables 

299 

300 ra_col = pexConfig.Field( 

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

302 dtype=str, 

303 default="ra", 

304 ) 

305 

306 dec_col = pexConfig.Field( 

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

308 dtype=str, 

309 default="dec", 

310 ) 

311 

312 bulge_semimajor_col = pexConfig.Field( 

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

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

315 dtype=str, 

316 default="bulge_semimajor", 

317 ) 

318 

319 bulge_axis_ratio_col = pexConfig.Field( 

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

321 "half-light ellipse.", 

322 dtype=str, 

323 default="bulge_axis_ratio", 

324 ) 

325 

326 bulge_pa_col = pexConfig.Field( 

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

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

329 "half-light ellipse.", 

330 dtype=str, 

331 default="bulge_pa", 

332 ) 

333 

334 bulge_n_col = pexConfig.Field( 

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

336 dtype=str, 

337 default="bulge_n", 

338 ) 

339 

340 disk_semimajor_col = pexConfig.Field( 

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

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

343 dtype=str, 

344 default="disk_semimajor", 

345 ) 

346 

347 disk_axis_ratio_col = pexConfig.Field( 

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

349 "half-light ellipse.", 

350 dtype=str, 

351 default="disk_axis_ratio", 

352 ) 

353 

354 disk_pa_col = pexConfig.Field( 

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

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

357 "half-light ellipse.", 

358 dtype=str, 

359 default="disk_pa", 

360 ) 

361 

362 disk_n_col = pexConfig.Field( 

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

364 dtype=str, 

365 default="disk_n", 

366 ) 

367 

368 bulge_disk_flux_ratio_col = pexConfig.Field( 

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

370 dtype=str, 

371 default="bulge_disk_flux_ratio", 

372 ) 

373 

374 mag_col = pexConfig.Field( 

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

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

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

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

379 dtype=str, 

380 default="%s_mag" 

381 ) 

382 

383 select_col = pexConfig.Field( 

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

385 "add.", 

386 dtype=str, 

387 default="select", 

388 ) 

389 

390 length_col = pexConfig.Field( 

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

392 dtype=str, 

393 default="trail_length", 

394 ) 

395 

396 angle_col = pexConfig.Field( 

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

398 dtype=str, 

399 default="trail_angle", 

400 ) 

401 

402 # Deprecated config variables 

403 

404 raColName = pexConfig.Field( 

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

406 dtype=str, 

407 default="raJ2000", 

408 deprecated="Use `ra_col` instead." 

409 ) 

410 

411 decColName = pexConfig.Field( 

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

413 dtype=str, 

414 default="decJ2000", 

415 deprecated="Use `dec_col` instead." 

416 ) 

417 

418 diskHLR = pexConfig.Field( 

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

420 dtype=str, 

421 default="DiskHalfLightRadius", 

422 deprecated=( 

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

424 " to specify disk half-light ellipse." 

425 ) 

426 ) 

427 

428 aDisk = pexConfig.Field( 

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

430 "catalog.", 

431 dtype=str, 

432 default="a_d", 

433 deprecated=( 

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

435 " to specify disk half-light ellipse." 

436 ) 

437 ) 

438 

439 bDisk = pexConfig.Field( 

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

441 dtype=str, 

442 default="b_d", 

443 deprecated=( 

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

445 " to specify disk half-light ellipse." 

446 ) 

447 ) 

448 

449 paDisk = pexConfig.Field( 

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

451 dtype=str, 

452 default="pa_disk", 

453 deprecated=( 

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

455 " to specify disk half-light ellipse." 

456 ) 

457 ) 

458 

459 nDisk = pexConfig.Field( 

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

461 dtype=str, 

462 default="disk_n", 

463 deprecated="Use `disk_n_col` instead." 

464 ) 

465 

466 bulgeHLR = pexConfig.Field( 

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

468 dtype=str, 

469 default="BulgeHalfLightRadius", 

470 deprecated=( 

471 "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and " 

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

473 ) 

474 ) 

475 

476 aBulge = pexConfig.Field( 

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

478 dtype=str, 

479 default="a_b", 

480 deprecated=( 

481 "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and " 

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

483 ) 

484 ) 

485 

486 bBulge = pexConfig.Field( 

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

488 "catalog.", 

489 dtype=str, 

490 default="b_b", 

491 deprecated=( 

492 "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and " 

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

494 ) 

495 ) 

496 

497 paBulge = pexConfig.Field( 

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

499 dtype=str, 

500 default="pa_bulge", 

501 deprecated=( 

502 "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and " 

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

504 ) 

505 ) 

506 

507 nBulge = pexConfig.Field( 

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

509 dtype=str, 

510 default="bulge_n", 

511 deprecated="Use `bulge_n_col` instead." 

512 ) 

513 

514 magVar = pexConfig.Field( 

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

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

517 dtype=str, 

518 default="%smagVar", 

519 deprecated="Use `mag_col` instead." 

520 ) 

521 

522 sourceSelectionColName = pexConfig.Field( 

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

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

525 dtype=str, 

526 default="templateSource", 

527 deprecated="Use `select_col` instead." 

528 ) 

529 

530 

531class InsertFakesTask(PipelineTask): 

532 """Insert fake objects into images. 

533 

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

535 from the specified file and then modelled using galsim. 

536 

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

538 image. 

539 

540 `addPixCoords` 

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

542 `mkFakeGalsimGalaxies` 

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

544 `mkFakeStars` 

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

546 input file. 

547 `cleanCat` 

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

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

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

551 to only those which are True in this column. 

552 `addFakeSources` 

553 Add the fake sources to the image. 

554 

555 """ 

556 

557 _DefaultName = "insertFakes" 

558 ConfigClass = InsertFakesConfig 

559 

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

561 inputs = butlerQC.get(inputRefs) 

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

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

564 

565 outputs = self.run(**inputs) 

566 butlerQC.put(outputs, outputRefs) 

567 

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

569 """Add fake sources to an image. 

570 

571 Parameters 

572 ---------- 

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

574 The catalog of fake sources to be input 

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

576 The image into which the fake sources should be added 

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

578 WCS to use to add fake sources 

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

580 Photometric calibration to be used to calibrate the fake sources 

581 

582 Returns 

583 ------- 

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

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

586 

587 Notes 

588 ----- 

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

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

591 

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

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

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

595 the image and the image with fakes included returned. 

596 

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

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

599 """ 

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

601 # so we can reset at the end. 

602 origWcs = image.getWcs() 

603 origPhotoCalib = image.getPhotoCalib() 

604 image.setWcs(wcs) 

605 image.setPhotoCalib(photoCalib) 

606 

607 band = image.getFilter().bandLabel 

608 fakeCat = self._standardizeColumns(fakeCat, band) 

609 

610 fakeCat = self.addPixCoords(fakeCat, image) 

611 fakeCat = self.trimFakeCat(fakeCat, image) 

612 

613 if len(fakeCat) > 0: 

614 if not self.config.insertImages: 

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

616 galCheckVal = "galaxy" 

617 starCheckVal = "star" 

618 trailCheckVal = "trail" 

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

620 galCheckVal = b"galaxy" 

621 starCheckVal = b"star" 

622 trailCheckVal = b"trail" 

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

624 galCheckVal = 1 

625 starCheckVal = 0 

626 trailCheckVal = 2 

627 else: 

628 raise TypeError( 

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

630 ) 

631 if self.config.doCleanCat: 

632 fakeCat = self.cleanCat(fakeCat, starCheckVal) 

633 

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

635 trailCheckVal) 

636 else: 

637 generator = self._generateGSObjectsFromImages(image, fakeCat) 

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

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

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

641 image.mask.addMaskPlane("FAKE") 

642 else: 

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

644 

645 # restore original exposure WCS and photoCalib 

646 image.setWcs(origWcs) 

647 image.setPhotoCalib(origPhotoCalib) 

648 

649 resultStruct = pipeBase.Struct(imageWithFakes=image) 

650 

651 return resultStruct 

652 

653 def _standardizeColumns(self, fakeCat, band): 

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

655 names in the input catalog. 

656 

657 Parameters 

658 ---------- 

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

660 The catalog of fake sources to be input 

661 band : `str` 

662 Label for the current band being processed. 

663 

664 Returns 

665 ------- 

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

667 The standardized catalog of fake sources 

668 """ 

669 cfg = self.config 

670 replace_dict = {} 

671 

672 def add_to_replace_dict(new_name, depr_name, std_name): 

673 if new_name in fakeCat.columns: 

674 replace_dict[new_name] = std_name 

675 elif depr_name in fakeCat.columns: 

676 replace_dict[depr_name] = std_name 

677 else: 

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

679 

680 # Prefer new config variables over deprecated config variables. 

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

682 for new_name, depr_name, std_name in [ 

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

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

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

686 ]: 

687 add_to_replace_dict(new_name, depr_name, std_name) 

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

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

690 for new_name, depr_name, std_name in [ 

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

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

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

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

695 ]: 

696 add_to_replace_dict(new_name, depr_name, std_name) 

697 

698 if cfg.doSubSelectSources: 

699 add_to_replace_dict( 

700 cfg.select_col, 

701 cfg.sourceSelectionColName, 

702 'select' 

703 ) 

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

705 

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

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

708 # Just handle these manually. 

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

710 if ( 

711 cfg.bulge_semimajor_col in fakeCat.columns 

712 and cfg.bulge_axis_ratio_col in fakeCat.columns 

713 ): 

714 fakeCat = fakeCat.rename( 

715 columns={ 

716 cfg.bulge_semimajor_col: 'bulge_semimajor', 

717 cfg.bulge_axis_ratio_col: 'bulge_axis_ratio', 

718 cfg.disk_semimajor_col: 'disk_semimajor', 

719 cfg.disk_axis_ratio_col: 'disk_axis_ratio', 

720 }, 

721 copy=False 

722 ) 

723 elif ( 

724 cfg.bulgeHLR in fakeCat.columns 

725 and cfg.aBulge in fakeCat.columns 

726 and cfg.bBulge in fakeCat.columns 

727 ): 

728 fakeCat['bulge_axis_ratio'] = ( 

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

730 ) 

731 fakeCat['bulge_semimajor'] = ( 

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

733 ) 

734 fakeCat['disk_axis_ratio'] = ( 

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

736 ) 

737 fakeCat['disk_semimajor'] = ( 

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

739 ) 

740 else: 

741 raise ValueError( 

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

743 "axis ratio." 

744 ) 

745 

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

747 if cfg.bulge_disk_flux_ratio_col in fakeCat.columns: 

748 fakeCat = fakeCat.rename( 

749 columns={ 

750 cfg.bulge_disk_flux_ratio_col: 'bulge_disk_flux_ratio' 

751 }, 

752 copy=False 

753 ) 

754 else: 

755 fakeCat['bulge_disk_flux_ratio'] = 1.0 

756 

757 return fakeCat 

758 

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

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

761 

762 Parameters 

763 ---------- 

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

765 The exposure into which the fake sources should be added 

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

767 The catalog of fake sources to be input 

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

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

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

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

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

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

774 

775 Yields 

776 ------ 

777 gsObjects : `generator` 

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

779 """ 

780 wcs = exposure.getWcs() 

781 photoCalib = exposure.getPhotoCalib() 

782 

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

784 

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

786 ra = row['ra'] 

787 dec = row['dec'] 

788 skyCoord = SpherePoint(ra, dec, radians) 

789 xy = wcs.skyToPixel(skyCoord) 

790 

791 try: 

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

793 except LogicError: 

794 continue 

795 

796 sourceType = row[self.config.sourceType] 

797 if sourceType == galCheckVal: 

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

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

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

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

802 

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

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

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

806 

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

808 gal = gal.withFlux(flux) 

809 

810 yield skyCoord, gal 

811 elif sourceType == starCheckVal: 

812 star = galsim.DeltaFunction() 

813 star = star.withFlux(flux) 

814 yield skyCoord, star 

815 elif sourceType == trailCheckVal: 

816 length = row['trail_length'] 

817 angle = row['trail_angle'] 

818 

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

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

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

822 trail = galsim.Box(length, thickness) 

823 trail = trail.rotate(theta) 

824 trail = trail.withFlux(flux*length) 

825 

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

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

828 # coordinates, we must transform the trail here. 

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

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

831 

832 yield skyCoord, trail 

833 else: 

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

835 

836 def _generateGSObjectsFromImages(self, exposure, fakeCat): 

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

838 

839 Parameters 

840 ---------- 

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

842 The exposure into which the fake sources should be added 

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

844 The catalog of fake sources to be input 

845 

846 Yields 

847 ------ 

848 gsObjects : `generator` 

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

850 """ 

851 band = exposure.getFilter().bandLabel 

852 wcs = exposure.getWcs() 

853 photoCalib = exposure.getPhotoCalib() 

854 

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

856 

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

858 ra = row['ra'] 

859 dec = row['dec'] 

860 skyCoord = SpherePoint(ra, dec, radians) 

861 xy = wcs.skyToPixel(skyCoord) 

862 

863 try: 

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

865 except LogicError: 

866 continue 

867 

868 imFile = row[band+"imFilename"] 

869 try: 

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

871 except AttributeError: 

872 pass 

873 imFile = imFile.strip() 

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

875 

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

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

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

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

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

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

882 # exception. 

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

884 raise RuntimeError( 

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

886 ) 

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

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

889 # position. 

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

891 mat = linWcs.getMatrix() 

892 im.wcs = galsim.JacobianWCS( 

893 mat[0, 0], mat[0, 1], mat[1, 0], mat[1, 1] 

894 ) 

895 else: 

896 raise ValueError( 

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

898 ) 

899 

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

901 obj = obj.withFlux(flux) 

902 yield skyCoord, obj 

903 

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

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

906 

907 Parameters 

908 ---------- 

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

910 The catalog of fake sources to be input 

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

912 WCS to use to add fake sources 

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

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

915 The PSF information to use to make the PSF images 

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

917 Photometric calibration to be used to calibrate the fake sources 

918 band : `str` 

919 The filter band that the observation was taken in. 

920 pixelScale : `float` 

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

922 

923 Returns 

924 ------- 

925 galImages : `list` 

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

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

928 For sources labelled as galaxy. 

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

933 

934 Notes 

935 ----- 

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

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

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

939 """ 

940 galImages = [] 

941 starImages = [] 

942 

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

944 

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

946 fakeCat["sourceType"].array, 

947 fakeCat['mag'].array, 

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

949 

950 im = afwImage.ImageF.readFits(imFile) 

951 

952 xy = geom.Point2D(x, y) 

953 

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

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

956 try: 

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

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

959 psfKernel /= correctedFlux 

960 

961 except InvalidParameterError: 

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

963 continue 

964 

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

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

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

968 

969 try: 

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

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

972 continue 

973 

974 imSum = np.sum(outIm) 

975 divIm = outIm/imSum 

976 

977 try: 

978 flux = photoCalib.magnitudeToInstFlux(mag, xy) 

979 except LogicError: 

980 flux = 0 

981 

982 imWithFlux = flux*divIm 

983 

984 if sourceType == b"galaxy": 

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

986 if sourceType == b"star": 

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

988 

989 return galImages, starImages 

990 

991 def addPixCoords(self, fakeCat, image): 

992 

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

994 

995 Parameters 

996 ---------- 

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

998 The catalog of fake sources to be input 

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

1000 The image into which the fake sources should be added 

1001 

1002 Returns 

1003 ------- 

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

1005 """ 

1006 wcs = image.getWcs() 

1007 ras = fakeCat['ra'].values 

1008 decs = fakeCat['dec'].values 

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

1010 fakeCat["x"] = xs 

1011 fakeCat["y"] = ys 

1012 

1013 return fakeCat 

1014 

1015 def trimFakeCat(self, fakeCat, image): 

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

1017 

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

1019 

1020 Parameters 

1021 ---------- 

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

1023 The catalog of fake sources to be input 

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

1025 The image into which the fake sources should be added 

1026 

1027 Returns 

1028 ------- 

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

1030 The original fakeCat trimmed to the area of the image 

1031 """ 

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

1033 

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

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

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

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

1038 

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

1040 

1041 # also filter on the image BBox in pixel space 

1042 xs = fakeCat["x"].values 

1043 ys = fakeCat["y"].values 

1044 

1045 isContainedXy = xs >= wideBbox.minX 

1046 isContainedXy &= xs <= wideBbox.maxX 

1047 isContainedXy &= ys >= wideBbox.minY 

1048 isContainedXy &= ys <= wideBbox.maxY 

1049 

1050 return fakeCat[isContainedRaDec & isContainedXy] 

1051 

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

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

1054 

1055 Parameters 

1056 ---------- 

1057 band : `str` 

1058 pixelScale : `float` 

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

1060 The PSF information to use to make the PSF images 

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

1062 The catalog of fake sources to be input 

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

1064 Photometric calibration to be used to calibrate the fake sources 

1065 

1066 Yields 

1067 ------ 

1068 galImages : `generator` 

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

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

1071 

1072 Notes 

1073 ----- 

1074 

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

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

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

1078 

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

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

1081 attached to the config options. 

1082 

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

1084 """ 

1085 

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

1087 

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

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

1090 

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

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

1093 try: 

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

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

1096 psfKernel /= correctedFlux 

1097 

1098 except InvalidParameterError: 

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

1100 continue 

1101 

1102 try: 

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

1104 except LogicError: 

1105 flux = 0 

1106 

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

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

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

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

1111 

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

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

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

1115 

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

1117 gal = gal.withFlux(flux) 

1118 

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

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

1121 try: 

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

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

1124 continue 

1125 

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

1127 

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

1129 

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

1131 

1132 Parameters 

1133 ---------- 

1134 band : `str` 

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

1136 The PSF information to use to make the PSF images 

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

1138 The catalog of fake sources to be input 

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

1140 The image into which the fake sources should be added 

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

1142 Photometric calibration to be used to calibrate the fake sources 

1143 

1144 Yields 

1145 ------ 

1146 starImages : `generator` 

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

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

1149 

1150 Notes 

1151 ----- 

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

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

1154 given calibration radius used in the photometric calibration step. 

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

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

1157 """ 

1158 

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

1160 

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

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

1163 

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

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

1166 try: 

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

1168 starIm = psf.computeImage(xy) 

1169 starIm /= correctedFlux 

1170 

1171 except InvalidParameterError: 

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

1173 continue 

1174 

1175 try: 

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

1177 except LogicError: 

1178 flux = 0 

1179 

1180 starIm *= flux 

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

1182 

1183 def cleanCat(self, fakeCat, starCheckVal): 

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

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

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

1187 

1188 Parameters 

1189 ---------- 

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

1191 The catalog of fake sources to be input 

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

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

1194 

1195 Returns 

1196 ------- 

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

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

1199 """ 

1200 

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

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

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

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

1205 fakeCat = fakeCat[rowsToKeep] 

1206 

1207 minN = galsim.Sersic._minimum_n 

1208 maxN = galsim.Sersic._maximum_n 

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

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

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

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

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

1214 numRowsNotUsed, minN, maxN) 

1215 fakeCat = fakeCat[rowsWithGoodSersic] 

1216 

1217 if self.config.doSubSelectSources: 

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

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

1220 fakeCat = fakeCat[fakeCat['select']] 

1221 

1222 return fakeCat 

1223 

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

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

1226 

1227 Parameters 

1228 ---------- 

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

1230 The image into which the fake sources should be added 

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

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

1233 and the locations they are to be inserted at. 

1234 sourceType : `str` 

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

1236 

1237 Returns 

1238 ------- 

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

1240 

1241 Notes 

1242 ----- 

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

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

1245 """ 

1246 

1247 imageBBox = image.getBBox() 

1248 imageMI = image.maskedImage 

1249 

1250 for (fakeImage, xy) in fakeImages: 

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

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

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

1254 if sourceType == "galaxy": 

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

1256 else: 

1257 interpFakeImage = fakeImage 

1258 

1259 interpFakeImBBox = interpFakeImage.getBBox() 

1260 interpFakeImBBox.clip(imageBBox) 

1261 

1262 if interpFakeImBBox.getArea() > 0: 

1263 imageMIView = imageMI[interpFakeImBBox] 

1264 clippedFakeImage = interpFakeImage[interpFakeImBBox] 

1265 clippedFakeImageMI = afwImage.MaskedImageF(clippedFakeImage) 

1266 clippedFakeImageMI.mask.set(self.bitmask) 

1267 imageMIView += clippedFakeImageMI 

1268 

1269 return image