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

408 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-18 09:04 +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 

43from deprecated.sphinx import deprecated 

44 

45 

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

47 """Add fake sources to the given exposure 

48 

49 Parameters 

50 ---------- 

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

52 The exposure into which the fake sources should be added 

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

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

55 surface brightness profiles to inject. 

56 calibFluxRadius : `float`, optional 

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

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

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

60 slot_CalibFlux_instFlux. 

61 logger : `logging.Logger`, optional 

62 Logger. 

63 """ 

64 exposure.mask.addMaskPlane("FAKE") 

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

66 if logger: 

67 logger.info("Adding mask plane with bitmask %s", bitmask) 

68 

69 wcs = exposure.getWcs() 

70 psf = exposure.getPsf() 

71 

72 bbox = exposure.getBBox() 

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

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

75 

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

77 

78 for spt, gsObj in objects: 

79 pt = wcs.skyToPixel(spt) 

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

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

82 if logger: 

83 logger.debug("Adding fake source at %s", pt) 

84 

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

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

87 

88 # This check is here because sometimes the WCS 

89 # is multivalued and objects that should not be 

90 # were being included. 

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

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

93 continue 

94 

95 try: 

96 psfArr = psf.computeKernelImage(pt).array 

97 apCorr = psf.computeApertureFlux(calibFluxRadius, pt) 

98 except InvalidParameterError: 

99 # Try mapping to nearest point contained in bbox. 

100 contained_pt = Point2D( 

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

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

103 ) 

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

105 if logger: 

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

107 continue 

108 # otherwise, try again with new point 

109 try: 

110 psfArr = psf.computeKernelImage(contained_pt).array 

111 apCorr = psf.computeApertureFlux(calibFluxRadius, contained_pt) 

112 except InvalidParameterError: 

113 if logger: 

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

115 continue 

116 

117 psfArr /= apCorr 

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

119 

120 conv = galsim.Convolve(gsObj, gsPSF) 

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

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

123 subBounds &= fullBounds 

124 

125 if subBounds.area() > 0: 

126 subImg = gsImg[subBounds] 

127 offset = posd - subBounds.true_center 

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

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

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

131 

132 conv.drawImage( 

133 subImg, 

134 add_to_image=True, 

135 offset=offset, 

136 wcs=gsWCS, 

137 method='no_pixel' 

138 ) 

139 

140 subBox = geom.Box2I( 

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

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

143 ) 

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

145 

146 

147def _isWCSGalsimDefault(wcs, hdr): 

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

149 or if it's just the galsim default. 

150 

151 Parameters 

152 ---------- 

153 wcs : galsim.BaseWCS 

154 Potentially default WCS. 

155 hdr : galsim.fits.FitsHeader 

156 Header as read in by galsim. 

157 

158 Returns 

159 ------- 

160 isDefault : bool 

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

162 """ 

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

164 return False 

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

166 return False 

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

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

169 for wcs_type in galsim.fitswcs.fits_wcs_types: 

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

171 try: 

172 wcs_type._readHeader(hdr) 

173 return False 

174 except Exception: 

175 pass 

176 else: 

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

178 

179 

180class InsertFakesConnections(PipelineTaskConnections, 

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

182 "fakesType": "fakes_"}, 

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

184 

185 image = cT.Input( 

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

187 name="{coaddName}Coadd", 

188 storageClass="ExposureF", 

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

190 ) 

191 

192 fakeCat = cT.Input( 

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

194 name="{fakesType}fakeSourceCat", 

195 storageClass="DataFrame", 

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

197 ) 

198 

199 imageWithFakes = cT.Output( 

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

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

202 storageClass="ExposureF", 

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

204 ) 

205 

206 

207@deprecated( 

208 reason="This task will be removed in v28.0 as it is replaced by `source_injection` tasks.", 

209 version="v28.0", 

210 category=FutureWarning, 

211) 

212class InsertFakesConfig(PipelineTaskConfig, 

213 pipelineConnections=InsertFakesConnections): 

214 """Config for inserting fake sources 

215 """ 

216 

217 # Unchanged 

218 

219 doCleanCat = pexConfig.Field( 

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

221 dtype=bool, 

222 default=True, 

223 ) 

224 

225 fakeType = pexConfig.Field( 

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

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

228 "catalog.", 

229 dtype=str, 

230 default="static", 

231 ) 

232 

233 calibFluxRadius = pexConfig.Field( 

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

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

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

237 dtype=float, 

238 default=12.0, 

239 ) 

240 

241 coaddName = pexConfig.Field( 

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

243 dtype=str, 

244 default="deep", 

245 ) 

246 

247 doSubSelectSources = pexConfig.Field( 

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

249 "set in the sourceSelectionColName config option.", 

250 dtype=bool, 

251 default=False 

252 ) 

253 

254 insertImages = pexConfig.Field( 

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

256 dtype=bool, 

257 default=False, 

258 ) 

259 

260 insertOnlyStars = pexConfig.Field( 

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

262 dtype=bool, 

263 default=False, 

264 ) 

265 

266 doProcessAllDataIds = pexConfig.Field( 

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

268 dtype=bool, 

269 default=False, 

270 ) 

271 

272 trimBuffer = pexConfig.Field( 

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

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

275 dtype=int, 

276 default=100, 

277 ) 

278 

279 sourceType = pexConfig.Field( 

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

281 dtype=str, 

282 default="sourceType", 

283 ) 

284 

285 fits_alignment = pexConfig.ChoiceField( 

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

287 dtype=str, 

288 allowed={ 

289 "wcs": ( 

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

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

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

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

294 "image." 

295 ), 

296 "pixel": ( 

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

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

299 "distances in the target image." 

300 ) 

301 }, 

302 default="pixel" 

303 ) 

304 

305 # New source catalog config variables 

306 

307 ra_col = pexConfig.Field( 

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

309 dtype=str, 

310 default="ra", 

311 ) 

312 

313 dec_col = pexConfig.Field( 

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

315 dtype=str, 

316 default="dec", 

317 ) 

318 

319 bulge_semimajor_col = pexConfig.Field( 

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

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

322 dtype=str, 

323 default="bulge_semimajor", 

324 ) 

325 

326 bulge_axis_ratio_col = pexConfig.Field( 

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

328 "half-light ellipse.", 

329 dtype=str, 

330 default="bulge_axis_ratio", 

331 ) 

332 

333 bulge_pa_col = pexConfig.Field( 

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

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

336 "half-light ellipse.", 

337 dtype=str, 

338 default="bulge_pa", 

339 ) 

340 

341 bulge_n_col = pexConfig.Field( 

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

343 dtype=str, 

344 default="bulge_n", 

345 ) 

346 

347 disk_semimajor_col = pexConfig.Field( 

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

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

350 dtype=str, 

351 default="disk_semimajor", 

352 ) 

353 

354 disk_axis_ratio_col = pexConfig.Field( 

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

356 "half-light ellipse.", 

357 dtype=str, 

358 default="disk_axis_ratio", 

359 ) 

360 

361 disk_pa_col = pexConfig.Field( 

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

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

364 "half-light ellipse.", 

365 dtype=str, 

366 default="disk_pa", 

367 ) 

368 

369 disk_n_col = pexConfig.Field( 

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

371 dtype=str, 

372 default="disk_n", 

373 ) 

374 

375 bulge_disk_flux_ratio_col = pexConfig.Field( 

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

377 dtype=str, 

378 default="bulge_disk_flux_ratio", 

379 ) 

380 

381 mag_col = pexConfig.Field( 

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

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

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

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

386 dtype=str, 

387 default="%s_mag" 

388 ) 

389 

390 select_col = pexConfig.Field( 

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

392 "add.", 

393 dtype=str, 

394 default="select", 

395 ) 

396 

397 length_col = pexConfig.Field( 

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

399 dtype=str, 

400 default="trail_length", 

401 ) 

402 

403 angle_col = pexConfig.Field( 

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

405 dtype=str, 

406 default="trail_angle", 

407 ) 

408 

409 # Deprecated config variables 

410 

411 raColName = pexConfig.Field( 

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

413 dtype=str, 

414 default="raJ2000", 

415 deprecated="Use `ra_col` instead." 

416 ) 

417 

418 decColName = pexConfig.Field( 

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

420 dtype=str, 

421 default="decJ2000", 

422 deprecated="Use `dec_col` instead." 

423 ) 

424 

425 diskHLR = pexConfig.Field( 

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

427 dtype=str, 

428 default="DiskHalfLightRadius", 

429 deprecated=( 

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

431 " to specify disk half-light ellipse." 

432 ) 

433 ) 

434 

435 aDisk = pexConfig.Field( 

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

437 "catalog.", 

438 dtype=str, 

439 default="a_d", 

440 deprecated=( 

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

442 " to specify disk half-light ellipse." 

443 ) 

444 ) 

445 

446 bDisk = pexConfig.Field( 

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

448 dtype=str, 

449 default="b_d", 

450 deprecated=( 

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

452 " to specify disk half-light ellipse." 

453 ) 

454 ) 

455 

456 paDisk = pexConfig.Field( 

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

458 dtype=str, 

459 default="pa_disk", 

460 deprecated=( 

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

462 " to specify disk half-light ellipse." 

463 ) 

464 ) 

465 

466 nDisk = pexConfig.Field( 

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

468 dtype=str, 

469 default="disk_n", 

470 deprecated="Use `disk_n_col` instead." 

471 ) 

472 

473 bulgeHLR = pexConfig.Field( 

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

475 dtype=str, 

476 default="BulgeHalfLightRadius", 

477 deprecated=( 

478 "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and " 

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

480 ) 

481 ) 

482 

483 aBulge = pexConfig.Field( 

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

485 dtype=str, 

486 default="a_b", 

487 deprecated=( 

488 "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and " 

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

490 ) 

491 ) 

492 

493 bBulge = pexConfig.Field( 

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

495 "catalog.", 

496 dtype=str, 

497 default="b_b", 

498 deprecated=( 

499 "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and " 

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

501 ) 

502 ) 

503 

504 paBulge = pexConfig.Field( 

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

506 dtype=str, 

507 default="pa_bulge", 

508 deprecated=( 

509 "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and " 

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

511 ) 

512 ) 

513 

514 nBulge = pexConfig.Field( 

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

516 dtype=str, 

517 default="bulge_n", 

518 deprecated="Use `bulge_n_col` instead." 

519 ) 

520 

521 magVar = pexConfig.Field( 

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

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

524 dtype=str, 

525 default="%smagVar", 

526 deprecated="Use `mag_col` instead." 

527 ) 

528 

529 sourceSelectionColName = pexConfig.Field( 

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

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

532 dtype=str, 

533 default="templateSource", 

534 deprecated="Use `select_col` instead." 

535 ) 

536 

537 

538@deprecated( 

539 reason="This task will be removed in v28.0 as it is replaced by `source_injection` tasks.", 

540 version="v28.0", 

541 category=FutureWarning, 

542) 

543class InsertFakesTask(PipelineTask): 

544 """Insert fake objects into images. 

545 

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

547 from the specified file and then modelled using galsim. 

548 

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

550 image. 

551 

552 `addPixCoords` 

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

554 `mkFakeGalsimGalaxies` 

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

556 `mkFakeStars` 

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

558 input file. 

559 `cleanCat` 

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

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

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

563 to only those which are True in this column. 

564 `addFakeSources` 

565 Add the fake sources to the image. 

566 

567 """ 

568 

569 _DefaultName = "insertFakes" 

570 ConfigClass = InsertFakesConfig 

571 

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

573 inputs = butlerQC.get(inputRefs) 

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

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

576 

577 outputs = self.run(**inputs) 

578 butlerQC.put(outputs, outputRefs) 

579 

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

581 """Add fake sources to an image. 

582 

583 Parameters 

584 ---------- 

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

586 The catalog of fake sources to be input 

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

588 The image into which the fake sources should be added 

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

590 WCS to use to add fake sources 

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

592 Photometric calibration to be used to calibrate the fake sources 

593 

594 Returns 

595 ------- 

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

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

598 

599 Notes 

600 ----- 

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

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

603 

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

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

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

607 the image and the image with fakes included returned. 

608 

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

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

611 """ 

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

613 # so we can reset at the end. 

614 origWcs = image.getWcs() 

615 origPhotoCalib = image.getPhotoCalib() 

616 image.setWcs(wcs) 

617 image.setPhotoCalib(photoCalib) 

618 

619 band = image.getFilter().bandLabel 

620 fakeCat = self._standardizeColumns(fakeCat, band) 

621 

622 fakeCat = self.addPixCoords(fakeCat, image) 

623 fakeCat = self.trimFakeCat(fakeCat, image) 

624 

625 if len(fakeCat) > 0: 

626 if not self.config.insertImages: 

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

628 galCheckVal = "galaxy" 

629 starCheckVal = "star" 

630 trailCheckVal = "trail" 

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

632 galCheckVal = b"galaxy" 

633 starCheckVal = b"star" 

634 trailCheckVal = b"trail" 

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

636 galCheckVal = 1 

637 starCheckVal = 0 

638 trailCheckVal = 2 

639 else: 

640 raise TypeError( 

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

642 ) 

643 if self.config.doCleanCat: 

644 fakeCat = self.cleanCat(fakeCat, starCheckVal) 

645 

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

647 trailCheckVal) 

648 else: 

649 generator = self._generateGSObjectsFromImages(image, fakeCat) 

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

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

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

653 image.mask.addMaskPlane("FAKE") 

654 else: 

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

656 

657 # restore original exposure WCS and photoCalib 

658 image.setWcs(origWcs) 

659 image.setPhotoCalib(origPhotoCalib) 

660 

661 resultStruct = pipeBase.Struct(imageWithFakes=image) 

662 

663 return resultStruct 

664 

665 def _standardizeColumns(self, fakeCat, band): 

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

667 names in the input catalog. 

668 

669 Parameters 

670 ---------- 

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

672 The catalog of fake sources to be input 

673 band : `str` 

674 Label for the current band being processed. 

675 

676 Returns 

677 ------- 

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

679 The standardized catalog of fake sources 

680 """ 

681 cfg = self.config 

682 replace_dict = {} 

683 

684 def add_to_replace_dict(new_name, depr_name, std_name): 

685 if new_name in fakeCat.columns: 

686 replace_dict[new_name] = std_name 

687 elif depr_name in fakeCat.columns: 

688 replace_dict[depr_name] = std_name 

689 else: 

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

691 

692 # Prefer new config variables over deprecated config variables. 

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

694 for new_name, depr_name, std_name in [ 

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

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

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

698 ]: 

699 add_to_replace_dict(new_name, depr_name, std_name) 

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

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

702 for new_name, depr_name, std_name in [ 

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

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

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

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

707 ]: 

708 add_to_replace_dict(new_name, depr_name, std_name) 

709 

710 if cfg.doSubSelectSources: 

711 add_to_replace_dict( 

712 cfg.select_col, 

713 cfg.sourceSelectionColName, 

714 'select' 

715 ) 

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

717 

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

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

720 # Just handle these manually. 

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

722 if ( 

723 cfg.bulge_semimajor_col in fakeCat.columns 

724 and cfg.bulge_axis_ratio_col in fakeCat.columns 

725 ): 

726 fakeCat = fakeCat.rename( 

727 columns={ 

728 cfg.bulge_semimajor_col: 'bulge_semimajor', 

729 cfg.bulge_axis_ratio_col: 'bulge_axis_ratio', 

730 cfg.disk_semimajor_col: 'disk_semimajor', 

731 cfg.disk_axis_ratio_col: 'disk_axis_ratio', 

732 }, 

733 copy=False 

734 ) 

735 elif ( 

736 cfg.bulgeHLR in fakeCat.columns 

737 and cfg.aBulge in fakeCat.columns 

738 and cfg.bBulge in fakeCat.columns 

739 ): 

740 fakeCat['bulge_axis_ratio'] = ( 

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

742 ) 

743 fakeCat['bulge_semimajor'] = ( 

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

745 ) 

746 fakeCat['disk_axis_ratio'] = ( 

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

748 ) 

749 fakeCat['disk_semimajor'] = ( 

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

751 ) 

752 else: 

753 raise ValueError( 

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

755 "axis ratio." 

756 ) 

757 

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

759 if cfg.bulge_disk_flux_ratio_col in fakeCat.columns: 

760 fakeCat = fakeCat.rename( 

761 columns={ 

762 cfg.bulge_disk_flux_ratio_col: 'bulge_disk_flux_ratio' 

763 }, 

764 copy=False 

765 ) 

766 else: 

767 fakeCat['bulge_disk_flux_ratio'] = 1.0 

768 

769 return fakeCat 

770 

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

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

773 

774 Parameters 

775 ---------- 

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

777 The exposure into which the fake sources should be added 

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

779 The catalog of fake sources to be input 

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

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

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

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

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

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

786 

787 Yields 

788 ------ 

789 gsObjects : `generator` 

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

791 """ 

792 wcs = exposure.getWcs() 

793 photoCalib = exposure.getPhotoCalib() 

794 

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

796 

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

798 ra = row['ra'] 

799 dec = row['dec'] 

800 skyCoord = SpherePoint(ra, dec, radians) 

801 xy = wcs.skyToPixel(skyCoord) 

802 

803 try: 

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

805 except LogicError: 

806 continue 

807 

808 sourceType = row[self.config.sourceType] 

809 if sourceType == galCheckVal: 

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

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

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

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

814 

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

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

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

818 

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

820 gal = gal.withFlux(flux) 

821 

822 yield skyCoord, gal 

823 elif sourceType == starCheckVal: 

824 star = galsim.DeltaFunction() 

825 star = star.withFlux(flux) 

826 yield skyCoord, star 

827 elif sourceType == trailCheckVal: 

828 length = row['trail_length'] 

829 angle = row['trail_angle'] 

830 

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

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

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

834 trail = galsim.Box(length, thickness) 

835 trail = trail.rotate(theta) 

836 trail = trail.withFlux(flux*length) 

837 

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

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

840 # coordinates, we must transform the trail here. 

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

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

843 

844 yield skyCoord, trail 

845 else: 

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

847 

848 def _generateGSObjectsFromImages(self, exposure, fakeCat): 

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

850 

851 Parameters 

852 ---------- 

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

854 The exposure into which the fake sources should be added 

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

856 The catalog of fake sources to be input 

857 

858 Yields 

859 ------ 

860 gsObjects : `generator` 

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

862 """ 

863 band = exposure.getFilter().bandLabel 

864 wcs = exposure.getWcs() 

865 photoCalib = exposure.getPhotoCalib() 

866 

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

868 

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

870 ra = row['ra'] 

871 dec = row['dec'] 

872 skyCoord = SpherePoint(ra, dec, radians) 

873 xy = wcs.skyToPixel(skyCoord) 

874 

875 try: 

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

877 except LogicError: 

878 continue 

879 

880 imFile = row[band+"imFilename"] 

881 try: 

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

883 except AttributeError: 

884 pass 

885 imFile = imFile.strip() 

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

887 

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

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

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

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

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

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

894 # exception. 

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

896 raise RuntimeError( 

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

898 ) 

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

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

901 # position. 

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

903 mat = linWcs.getMatrix() 

904 im.wcs = galsim.JacobianWCS( 

905 mat[0, 0], mat[0, 1], mat[1, 0], mat[1, 1] 

906 ) 

907 else: 

908 raise ValueError( 

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

910 ) 

911 

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

913 obj = obj.withFlux(flux) 

914 yield skyCoord, obj 

915 

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

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

918 

919 Parameters 

920 ---------- 

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

922 The catalog of fake sources to be input 

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

924 WCS to use to add fake sources 

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

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

927 The PSF information to use to make the PSF images 

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

929 Photometric calibration to be used to calibrate the fake sources 

930 band : `str` 

931 The filter band that the observation was taken in. 

932 pixelScale : `float` 

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

934 

935 Returns 

936 ------- 

937 galImages : `list` 

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

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

940 For sources labelled as galaxy. 

941 starImages : `list` 

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

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

944 For sources labelled as star. 

945 

946 Notes 

947 ----- 

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

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

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

951 """ 

952 galImages = [] 

953 starImages = [] 

954 

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

956 

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

958 fakeCat["sourceType"].array, 

959 fakeCat['mag'].array, 

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

961 

962 im = afwImage.ImageF.readFits(imFile) 

963 

964 xy = geom.Point2D(x, y) 

965 

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

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

968 try: 

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

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

971 psfKernel /= correctedFlux 

972 

973 except InvalidParameterError: 

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

975 continue 

976 

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

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

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

980 

981 try: 

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

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

984 continue 

985 

986 imSum = np.sum(outIm) 

987 divIm = outIm/imSum 

988 

989 try: 

990 flux = photoCalib.magnitudeToInstFlux(mag, xy) 

991 except LogicError: 

992 flux = 0 

993 

994 imWithFlux = flux*divIm 

995 

996 if sourceType == b"galaxy": 

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

998 if sourceType == b"star": 

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

1000 

1001 return galImages, starImages 

1002 

1003 def addPixCoords(self, fakeCat, image): 

1004 

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

1006 

1007 Parameters 

1008 ---------- 

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

1010 The catalog of fake sources to be input 

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

1012 The image into which the fake sources should be added 

1013 

1014 Returns 

1015 ------- 

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

1017 """ 

1018 wcs = image.getWcs() 

1019 ras = fakeCat['ra'].values 

1020 decs = fakeCat['dec'].values 

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

1022 fakeCat["x"] = xs 

1023 fakeCat["y"] = ys 

1024 

1025 return fakeCat 

1026 

1027 def trimFakeCat(self, fakeCat, image): 

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

1029 

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

1031 

1032 Parameters 

1033 ---------- 

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

1035 The catalog of fake sources to be input 

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

1037 The image into which the fake sources should be added 

1038 

1039 Returns 

1040 ------- 

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

1042 The original fakeCat trimmed to the area of the image 

1043 """ 

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

1045 

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

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

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

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

1050 

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

1052 

1053 # also filter on the image BBox in pixel space 

1054 xs = fakeCat["x"].values 

1055 ys = fakeCat["y"].values 

1056 

1057 isContainedXy = xs >= wideBbox.minX 

1058 isContainedXy &= xs <= wideBbox.maxX 

1059 isContainedXy &= ys >= wideBbox.minY 

1060 isContainedXy &= ys <= wideBbox.maxY 

1061 

1062 return fakeCat[isContainedRaDec & isContainedXy] 

1063 

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

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

1066 

1067 Parameters 

1068 ---------- 

1069 band : `str` 

1070 pixelScale : `float` 

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

1072 The PSF information to use to make the PSF images 

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

1074 The catalog of fake sources to be input 

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

1076 Photometric calibration to be used to calibrate the fake sources 

1077 

1078 Yields 

1079 ------ 

1080 galImages : `generator` 

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

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

1083 

1084 Notes 

1085 ----- 

1086 

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

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

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

1090 

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

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

1093 attached to the config options. 

1094 

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

1096 """ 

1097 

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

1099 

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

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

1102 

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

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

1105 try: 

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

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

1108 psfKernel /= correctedFlux 

1109 

1110 except InvalidParameterError: 

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

1112 continue 

1113 

1114 try: 

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

1116 except LogicError: 

1117 flux = 0 

1118 

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

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

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

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

1123 

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

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

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

1127 

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

1129 gal = gal.withFlux(flux) 

1130 

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

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

1133 try: 

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

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

1136 continue 

1137 

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

1139 

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

1141 

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

1143 

1144 Parameters 

1145 ---------- 

1146 band : `str` 

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

1148 The PSF information to use to make the PSF images 

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

1150 The catalog of fake sources to be input 

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

1152 The image into which the fake sources should be added 

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

1154 Photometric calibration to be used to calibrate the fake sources 

1155 

1156 Yields 

1157 ------ 

1158 starImages : `generator` 

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

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

1161 

1162 Notes 

1163 ----- 

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

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

1166 given calibration radius used in the photometric calibration step. 

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

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

1169 """ 

1170 

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

1172 

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

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

1175 

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

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

1178 try: 

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

1180 starIm = psf.computeImage(xy) 

1181 starIm /= correctedFlux 

1182 

1183 except InvalidParameterError: 

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

1185 continue 

1186 

1187 try: 

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

1189 except LogicError: 

1190 flux = 0 

1191 

1192 starIm *= flux 

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

1194 

1195 def cleanCat(self, fakeCat, starCheckVal): 

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

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

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

1199 

1200 Parameters 

1201 ---------- 

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

1203 The catalog of fake sources to be input 

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

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

1206 

1207 Returns 

1208 ------- 

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

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

1211 """ 

1212 

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

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

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

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

1217 fakeCat = fakeCat[rowsToKeep] 

1218 

1219 minN = galsim.Sersic._minimum_n 

1220 maxN = galsim.Sersic._maximum_n 

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

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

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

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

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

1226 numRowsNotUsed, minN, maxN) 

1227 fakeCat = fakeCat[rowsWithGoodSersic] 

1228 

1229 if self.config.doSubSelectSources: 

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

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

1232 fakeCat = fakeCat[fakeCat['select']] 

1233 

1234 return fakeCat 

1235 

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

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

1238 

1239 Parameters 

1240 ---------- 

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

1242 The image into which the fake sources should be added 

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

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

1245 and the locations they are to be inserted at. 

1246 sourceType : `str` 

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

1248 

1249 Returns 

1250 ------- 

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

1252 

1253 Notes 

1254 ----- 

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

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

1257 """ 

1258 

1259 imageBBox = image.getBBox() 

1260 imageMI = image.maskedImage 

1261 

1262 for (fakeImage, xy) in fakeImages: 

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

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

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

1266 if sourceType == "galaxy": 

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

1268 else: 

1269 interpFakeImage = fakeImage 

1270 

1271 interpFakeImBBox = interpFakeImage.getBBox() 

1272 interpFakeImBBox.clip(imageBBox) 

1273 

1274 if interpFakeImBBox.getArea() > 0: 

1275 imageMIView = imageMI[interpFakeImBBox] 

1276 clippedFakeImage = interpFakeImage[interpFakeImBBox] 

1277 clippedFakeImageMI = afwImage.MaskedImageF(clippedFakeImage) 

1278 clippedFakeImageMI.mask.set(self.bitmask) 

1279 imageMIView += clippedFakeImageMI 

1280 

1281 return image