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

387 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-07-17 08:55 +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# (http://www.lsst.org). 

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

7# for details of code ownership. 

8# 

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

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

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

12# (at your option) any later version. 

13# 

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

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

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

17# GNU General Public License for more details. 

18# 

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

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

21 

22""" 

23Insert fakes into deepCoadds 

24""" 

25import galsim 

26from astropy.table import Table 

27import numpy as np 

28 

29import lsst.geom as geom 

30import lsst.afw.image as afwImage 

31import lsst.afw.math as afwMath 

32import lsst.pex.config as pexConfig 

33import lsst.pipe.base as pipeBase 

34 

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

36import lsst.pipe.base.connectionTypes as cT 

37from lsst.pex.exceptions import LogicError, InvalidParameterError 

38from lsst.coadd.utils.coaddDataIdContainer import ExistingCoaddDataIdContainer 

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

40 

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

42 

43 

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

45 """Add fake sources to the given exposure 

46 

47 Parameters 

48 ---------- 

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

50 The exposure into which the fake sources should be added 

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

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

53 surface brightness profiles to inject. 

54 calibFluxRadius : `float`, optional 

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

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

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

58 slot_CalibFlux_instFlux. 

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

60 Logger. 

61 """ 

62 exposure.mask.addMaskPlane("FAKE") 

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

64 if logger: 

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

66 

67 wcs = exposure.getWcs() 

68 psf = exposure.getPsf() 

69 

70 bbox = exposure.getBBox() 

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

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

73 

74 for spt, gsObj in objects: 

75 pt = wcs.skyToPixel(spt) 

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

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

78 if logger: 

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

80 

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

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

83 

84 try: 

85 psfArr = psf.computeKernelImage(pt).array 

86 except InvalidParameterError: 

87 # Try mapping to nearest point contained in bbox. 

88 contained_pt = Point2D( 

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

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

91 ) 

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

93 if logger: 

94 logger.infof( 

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

96 pt 

97 ) 

98 continue 

99 # otherwise, try again with new point 

100 try: 

101 psfArr = psf.computeKernelImage(contained_pt).array 

102 except InvalidParameterError: 

103 if logger: 

104 logger.infof( 

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

106 pt 

107 ) 

108 continue 

109 apCorr = psf.computeApertureFlux(calibFluxRadius) 

110 psfArr /= apCorr 

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

112 

113 conv = galsim.Convolve(gsObj, gsPSF) 

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

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

116 subBounds &= fullBounds 

117 

118 if subBounds.area() > 0: 

119 subImg = gsImg[subBounds] 

120 offset = posd - subBounds.true_center 

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

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

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

124 conv.drawImage( 

125 subImg, 

126 add_to_image=True, 

127 offset=offset, 

128 wcs=gsWCS, 

129 method='no_pixel' 

130 ) 

131 

132 subBox = geom.Box2I( 

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

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

135 ) 

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

137 

138 

139def _isWCSGalsimDefault(wcs, hdr): 

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

141 or if it's just the galsim default. 

142 

143 Parameters 

144 ---------- 

145 wcs : galsim.BaseWCS 

146 Potentially default WCS. 

147 hdr : galsim.fits.FitsHeader 

148 Header as read in by galsim. 

149 

150 Returns 

151 ------- 

152 isDefault : bool 

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

154 """ 

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

156 return False 

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

158 return False 

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

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

161 for wcs_type in galsim.fitswcs.fits_wcs_types: 

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

163 try: 

164 wcs_type._readHeader(hdr) 

165 return False 

166 except Exception: 

167 pass 

168 else: 

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

170 

171 

172class InsertFakesConnections(PipelineTaskConnections, 

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

174 "fakesType": "fakes_"}, 

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

176 

177 image = cT.Input( 

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

179 name="{coaddName}Coadd", 

180 storageClass="ExposureF", 

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

182 ) 

183 

184 fakeCat = cT.Input( 

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

186 name="{fakesType}fakeSourceCat", 

187 storageClass="DataFrame", 

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

189 ) 

190 

191 imageWithFakes = cT.Output( 

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

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

194 storageClass="ExposureF", 

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

196 ) 

197 

198 

199class InsertFakesConfig(PipelineTaskConfig, 

200 pipelineConnections=InsertFakesConnections): 

201 """Config for inserting fake sources 

202 """ 

203 

204 # Unchanged 

205 

206 doCleanCat = pexConfig.Field( 

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

208 dtype=bool, 

209 default=True, 

210 ) 

211 

212 fakeType = pexConfig.Field( 

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

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

215 "catalog.", 

216 dtype=str, 

217 default="static", 

218 ) 

219 

220 calibFluxRadius = pexConfig.Field( 

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

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

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

224 dtype=float, 

225 default=12.0, 

226 ) 

227 

228 coaddName = pexConfig.Field( 

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

230 dtype=str, 

231 default="deep", 

232 ) 

233 

234 doSubSelectSources = pexConfig.Field( 

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

236 "set in the sourceSelectionColName config option.", 

237 dtype=bool, 

238 default=False 

239 ) 

240 

241 insertImages = pexConfig.Field( 

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

243 dtype=bool, 

244 default=False, 

245 ) 

246 

247 doProcessAllDataIds = pexConfig.Field( 

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

249 dtype=bool, 

250 default=False, 

251 ) 

252 

253 trimBuffer = pexConfig.Field( 

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

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

256 dtype=int, 

257 default=100, 

258 ) 

259 

260 sourceType = pexConfig.Field( 

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

262 dtype=str, 

263 default="sourceType", 

264 ) 

265 

266 # New source catalog config variables 

267 

268 ra_col = pexConfig.Field( 

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

270 dtype=str, 

271 default="ra", 

272 ) 

273 

274 dec_col = pexConfig.Field( 

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

276 dtype=str, 

277 default="dec", 

278 ) 

279 

280 bulge_semimajor_col = pexConfig.Field( 

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

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

283 dtype=str, 

284 default="bulge_semimajor", 

285 ) 

286 

287 bulge_axis_ratio_col = pexConfig.Field( 

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

289 "half-light ellipse.", 

290 dtype=str, 

291 default="bulge_axis_ratio", 

292 ) 

293 

294 bulge_pa_col = pexConfig.Field( 

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

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

297 "half-light ellipse.", 

298 dtype=str, 

299 default="bulge_pa", 

300 ) 

301 

302 bulge_n_col = pexConfig.Field( 

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

304 dtype=str, 

305 default="bulge_n", 

306 ) 

307 

308 disk_semimajor_col = pexConfig.Field( 

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

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

311 dtype=str, 

312 default="disk_semimajor", 

313 ) 

314 

315 disk_axis_ratio_col = pexConfig.Field( 

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

317 "half-light ellipse.", 

318 dtype=str, 

319 default="disk_axis_ratio", 

320 ) 

321 

322 disk_pa_col = pexConfig.Field( 

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

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

325 "half-light ellipse.", 

326 dtype=str, 

327 default="disk_pa", 

328 ) 

329 

330 disk_n_col = pexConfig.Field( 

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

332 dtype=str, 

333 default="disk_n", 

334 ) 

335 

336 bulge_disk_flux_ratio_col = pexConfig.Field( 

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

338 dtype=str, 

339 default="bulge_disk_flux_ratio", 

340 ) 

341 

342 mag_col = pexConfig.Field( 

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

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

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

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

347 dtype=str, 

348 default="%s_mag" 

349 ) 

350 

351 select_col = pexConfig.Field( 

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

353 "add.", 

354 dtype=str, 

355 default="select", 

356 ) 

357 

358 # Deprecated config variables 

359 

360 raColName = pexConfig.Field( 

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

362 dtype=str, 

363 default="raJ2000", 

364 deprecated="Use `ra_col` instead." 

365 ) 

366 

367 decColName = pexConfig.Field( 

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

369 dtype=str, 

370 default="decJ2000", 

371 deprecated="Use `dec_col` instead." 

372 ) 

373 

374 diskHLR = pexConfig.Field( 

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

376 dtype=str, 

377 default="DiskHalfLightRadius", 

378 deprecated=( 

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

380 " to specify disk half-light ellipse." 

381 ) 

382 ) 

383 

384 aDisk = pexConfig.Field( 

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

386 "catalog.", 

387 dtype=str, 

388 default="a_d", 

389 deprecated=( 

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

391 " to specify disk half-light ellipse." 

392 ) 

393 ) 

394 

395 bDisk = pexConfig.Field( 

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

397 dtype=str, 

398 default="b_d", 

399 deprecated=( 

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

401 " to specify disk half-light ellipse." 

402 ) 

403 ) 

404 

405 paDisk = pexConfig.Field( 

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

407 dtype=str, 

408 default="pa_disk", 

409 deprecated=( 

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

411 " to specify disk half-light ellipse." 

412 ) 

413 ) 

414 

415 nDisk = pexConfig.Field( 

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

417 dtype=str, 

418 default="disk_n", 

419 deprecated="Use `disk_n` instead." 

420 ) 

421 

422 bulgeHLR = pexConfig.Field( 

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

424 dtype=str, 

425 default="BulgeHalfLightRadius", 

426 deprecated=( 

427 "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and " 

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

429 ) 

430 ) 

431 

432 aBulge = pexConfig.Field( 

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

434 dtype=str, 

435 default="a_b", 

436 deprecated=( 

437 "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and " 

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

439 ) 

440 ) 

441 

442 bBulge = pexConfig.Field( 

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

444 "catalog.", 

445 dtype=str, 

446 default="b_b", 

447 deprecated=( 

448 "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and " 

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

450 ) 

451 ) 

452 

453 paBulge = pexConfig.Field( 

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

455 dtype=str, 

456 default="pa_bulge", 

457 deprecated=( 

458 "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and " 

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

460 ) 

461 ) 

462 

463 nBulge = pexConfig.Field( 

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

465 dtype=str, 

466 default="bulge_n", 

467 deprecated="Use `bulge_n` instead." 

468 ) 

469 

470 magVar = pexConfig.Field( 

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

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

473 dtype=str, 

474 default="%smagVar", 

475 deprecated="Use `mag_col` instead." 

476 ) 

477 

478 sourceSelectionColName = pexConfig.Field( 

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

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

481 dtype=str, 

482 default="templateSource", 

483 deprecated="Use `select_col` instead." 

484 ) 

485 

486 

487class InsertFakesTask(PipelineTask, CmdLineTask): 

488 """Insert fake objects into images. 

489 

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

491 from the specified file and then modelled using galsim. 

492 

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

494 image. 

495 

496 `addPixCoords` 

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

498 `mkFakeGalsimGalaxies` 

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

500 `mkFakeStars` 

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

502 input file. 

503 `cleanCat` 

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

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

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

507 to only those which are True in this column. 

508 `addFakeSources` 

509 Add the fake sources to the image. 

510 

511 """ 

512 

513 _DefaultName = "insertFakes" 

514 ConfigClass = InsertFakesConfig 

515 

516 def runDataRef(self, dataRef): 

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

518 

519 Parameters 

520 ---------- 

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

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

523 Used to access the following data products: 

524 deepCoadd 

525 """ 

526 

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

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

529 

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

531 

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

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

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

535 # task structure for ref cats is in place. 

536 self.fakeSourceCatType = "deepCoadd_fakeSourceCat" 

537 else: 

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

539 

540 coadd = dataRef.get("deepCoadd") 

541 wcs = coadd.getWcs() 

542 photoCalib = coadd.getPhotoCalib() 

543 

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

545 

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

547 

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

549 inputs = butlerQC.get(inputRefs) 

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

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

552 

553 outputs = self.run(**inputs) 

554 butlerQC.put(outputs, outputRefs) 

555 

556 @classmethod 

557 def _makeArgumentParser(cls): 

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

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

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

561 ContainerClass=ExistingCoaddDataIdContainer) 

562 return parser 

563 

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

565 """Add fake sources to an image. 

566 

567 Parameters 

568 ---------- 

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

570 The catalog of fake sources to be input 

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

572 The image into which the fake sources should be added 

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

574 WCS to use to add fake sources 

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

576 Photometric calibration to be used to calibrate the fake sources 

577 

578 Returns 

579 ------- 

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

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

582 

583 Notes 

584 ----- 

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

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

587 

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

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

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

591 the image and the image with fakes included returned. 

592 

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

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

595 """ 

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

597 # so we can reset at the end. 

598 origWcs = image.getWcs() 

599 origPhotoCalib = image.getPhotoCalib() 

600 image.setWcs(wcs) 

601 image.setPhotoCalib(photoCalib) 

602 

603 band = image.getFilterLabel().bandLabel 

604 fakeCat = self._standardizeColumns(fakeCat, band) 

605 

606 fakeCat = self.addPixCoords(fakeCat, image) 

607 fakeCat = self.trimFakeCat(fakeCat, image) 

608 

609 if len(fakeCat) > 0: 

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

611 galCheckVal = "galaxy" 

612 starCheckVal = "star" 

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

614 galCheckVal = b"galaxy" 

615 starCheckVal = b"star" 

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

617 galCheckVal = 1 

618 starCheckVal = 0 

619 else: 

620 raise TypeError("sourceType column does not have required type, should be str, bytes or int") 

621 

622 if not self.config.insertImages: 

623 if self.config.doCleanCat: 

624 fakeCat = self.cleanCat(fakeCat, starCheckVal) 

625 

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

627 else: 

628 generator = self._generateGSObjectsFromImages(image, fakeCat) 

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

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

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

632 image.mask.addMaskPlane("FAKE") 

633 else: 

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

635 

636 # restore original exposure WCS and photoCalib 

637 image.setWcs(origWcs) 

638 image.setPhotoCalib(origPhotoCalib) 

639 

640 resultStruct = pipeBase.Struct(imageWithFakes=image) 

641 

642 return resultStruct 

643 

644 def _standardizeColumns(self, fakeCat, band): 

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

646 names in the input catalog. 

647 

648 Parameters 

649 ---------- 

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

651 The catalog of fake sources to be input 

652 band : `str` 

653 Label for the current band being processed. 

654 

655 Returns 

656 ------- 

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

658 The standardized catalog of fake sources 

659 """ 

660 cfg = self.config 

661 replace_dict = {} 

662 

663 # Prefer new config variables over deprecated config variables. 

664 # The following are fairly simple to handle as they're just column name 

665 # changes. 

666 for new_name, depr_name, std_name in [ 

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

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

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

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

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

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

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

674 (cfg.select_col, cfg.sourceSelectionColName, 'select') 

675 ]: 

676 # Only standardize "select" column if doSubSelectSources is True 

677 if not cfg.doSubSelectSources and std_name == 'select': 

678 continue 

679 if new_name in fakeCat.columns: 

680 replace_dict[new_name] = std_name 

681 elif depr_name in fakeCat.columns: 

682 replace_dict[depr_name] = std_name 

683 else: 

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

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

686 

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

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

689 # Just handle these manually. 

690 if ( 

691 cfg.bulge_semimajor_col in fakeCat.columns 

692 and cfg.bulge_axis_ratio_col in fakeCat.columns 

693 ): 

694 fakeCat = fakeCat.rename( 

695 columns={ 

696 cfg.bulge_semimajor_col: 'bulge_semimajor', 

697 cfg.bulge_axis_ratio_col: 'bulge_axis_ratio', 

698 cfg.disk_semimajor_col: 'disk_semimajor', 

699 cfg.disk_axis_ratio_col: 'disk_axis_ratio', 

700 }, 

701 copy=False 

702 ) 

703 elif ( 

704 cfg.bulgeHLR in fakeCat.columns 

705 and cfg.aBulge in fakeCat.columns 

706 and cfg.bBulge in fakeCat.columns 

707 ): 

708 fakeCat['bulge_axis_ratio'] = ( 

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

710 ) 

711 fakeCat['bulge_semimajor'] = ( 

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

713 ) 

714 fakeCat['disk_axis_ratio'] = ( 

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

716 ) 

717 fakeCat['disk_semimajor'] = ( 

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

719 ) 

720 else: 

721 raise ValueError( 

722 "Could not determine columns for half-light radius and axis " 

723 "ratio." 

724 ) 

725 

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

727 if cfg.bulge_disk_flux_ratio_col in fakeCat.columns: 

728 fakeCat = fakeCat.rename( 

729 columns={ 

730 cfg.bulge_disk_flux_ratio_col: 'bulge_disk_flux_ratio' 

731 }, 

732 copy=False 

733 ) 

734 else: 

735 fakeCat['bulge_disk_flux_ratio'] = 1.0 

736 

737 return fakeCat 

738 

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

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

741 

742 Parameters 

743 ---------- 

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

745 The exposure into which the fake sources should be added 

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

747 The catalog of fake sources to be input 

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

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

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

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

752 

753 Yields 

754 ------ 

755 gsObjects : `generator` 

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

757 """ 

758 wcs = exposure.getWcs() 

759 photoCalib = exposure.getPhotoCalib() 

760 

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

762 

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

764 ra = row['ra'] 

765 dec = row['dec'] 

766 skyCoord = SpherePoint(ra, dec, radians) 

767 xy = wcs.skyToPixel(skyCoord) 

768 

769 try: 

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

771 except LogicError: 

772 continue 

773 

774 sourceType = row[self.config.sourceType] 

775 if sourceType == galCheckVal: 

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

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

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

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

780 

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

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

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

784 

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

786 gal = gal.withFlux(flux) 

787 

788 yield skyCoord, gal 

789 elif sourceType == starCheckVal: 

790 star = galsim.DeltaFunction() 

791 star = star.withFlux(flux) 

792 yield skyCoord, star 

793 else: 

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

795 

796 def _generateGSObjectsFromImages(self, exposure, fakeCat): 

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

798 

799 Parameters 

800 ---------- 

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

802 The exposure into which the fake sources should be added 

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

804 The catalog of fake sources to be input 

805 

806 Yields 

807 ------ 

808 gsObjects : `generator` 

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

810 """ 

811 band = exposure.getFilterLabel().bandLabel 

812 wcs = exposure.getWcs() 

813 photoCalib = exposure.getPhotoCalib() 

814 

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

816 

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

818 ra = row['ra'] 

819 dec = row['dec'] 

820 skyCoord = SpherePoint(ra, dec, radians) 

821 xy = wcs.skyToPixel(skyCoord) 

822 

823 try: 

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

825 except LogicError: 

826 continue 

827 

828 imFile = row[band+"imFilename"] 

829 try: 

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

831 except AttributeError: 

832 pass 

833 imFile = imFile.strip() 

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

835 

836 # GalSim will always attach a WCS to the image read in as above. If 

837 # it can't find a WCS in the header, then it defaults to scale = 1.0 

838 # arcsec / pix. So if that's the scale, then we need to check if it 

839 # was explicitly set or if it's just the default. If it's just the 

840 # default then we should override with the pixel scale of the target 

841 # image. 

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

843 im.wcs = galsim.PixelScale( 

844 wcs.getPixelScale().asArcseconds() 

845 ) 

846 

847 obj = galsim.InterpolatedImage(im) 

848 obj = obj.withFlux(flux) 

849 yield skyCoord, obj 

850 

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

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

853 

854 Parameters 

855 ---------- 

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

857 The catalog of fake sources to be input 

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

859 WCS to use to add fake sources 

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

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

862 The PSF information to use to make the PSF images 

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

864 Photometric calibration to be used to calibrate the fake sources 

865 band : `str` 

866 The filter band that the observation was taken in. 

867 pixelScale : `float` 

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

869 

870 Returns 

871 ------- 

872 galImages : `list` 

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

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

875 For sources labelled as galaxy. 

876 starImages : `list` 

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

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

879 For sources labelled as star. 

880 

881 Notes 

882 ----- 

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

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

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

886 """ 

887 galImages = [] 

888 starImages = [] 

889 

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

891 

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

893 fakeCat["sourceType"].array, 

894 fakeCat['mag'].array, 

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

896 

897 im = afwImage.ImageF.readFits(imFile) 

898 

899 xy = geom.Point2D(x, y) 

900 

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

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

903 try: 

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

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

906 psfKernel /= correctedFlux 

907 

908 except InvalidParameterError: 

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

910 continue 

911 

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

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

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

915 

916 try: 

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

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

919 continue 

920 

921 imSum = np.sum(outIm) 

922 divIm = outIm/imSum 

923 

924 try: 

925 flux = photoCalib.magnitudeToInstFlux(mag, xy) 

926 except LogicError: 

927 flux = 0 

928 

929 imWithFlux = flux*divIm 

930 

931 if sourceType == b"galaxy": 

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

933 if sourceType == b"star": 

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

935 

936 return galImages, starImages 

937 

938 def addPixCoords(self, fakeCat, image): 

939 

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

941 

942 Parameters 

943 ---------- 

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

945 The catalog of fake sources to be input 

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

947 The image into which the fake sources should be added 

948 

949 Returns 

950 ------- 

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

952 """ 

953 wcs = image.getWcs() 

954 ras = fakeCat['ra'].values 

955 decs = fakeCat['dec'].values 

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

957 fakeCat["x"] = xs 

958 fakeCat["y"] = ys 

959 

960 return fakeCat 

961 

962 def trimFakeCat(self, fakeCat, image): 

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

964 

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

966 

967 Parameters 

968 ---------- 

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

970 The catalog of fake sources to be input 

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

972 The image into which the fake sources should be added 

973 

974 Returns 

975 ------- 

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

977 The original fakeCat trimmed to the area of the image 

978 """ 

979 

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

981 xs = fakeCat["x"].values 

982 ys = fakeCat["y"].values 

983 

984 isContained = xs >= bbox.minX 

985 isContained &= xs <= bbox.maxX 

986 isContained &= ys >= bbox.minY 

987 isContained &= ys <= bbox.maxY 

988 

989 return fakeCat[isContained] 

990 

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

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

993 

994 Parameters 

995 ---------- 

996 band : `str` 

997 pixelScale : `float` 

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

999 The PSF information to use to make the PSF images 

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

1001 The catalog of fake sources to be input 

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

1003 Photometric calibration to be used to calibrate the fake sources 

1004 

1005 Yields 

1006 ------- 

1007 galImages : `generator` 

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

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

1010 

1011 Notes 

1012 ----- 

1013 

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

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

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

1017 

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

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

1020 attached to the config options. 

1021 

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

1023 """ 

1024 

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

1026 

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

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

1029 

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

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

1032 try: 

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

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

1035 psfKernel /= correctedFlux 

1036 

1037 except InvalidParameterError: 

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

1039 continue 

1040 

1041 try: 

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

1043 except LogicError: 

1044 flux = 0 

1045 

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

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

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

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

1050 

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

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

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

1054 

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

1056 gal = gal.withFlux(flux) 

1057 

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

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

1060 try: 

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

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

1063 continue 

1064 

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

1066 

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

1068 

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

1070 

1071 Parameters 

1072 ---------- 

1073 band : `str` 

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

1075 The PSF information to use to make the PSF images 

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

1077 The catalog of fake sources to be input 

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

1079 The image into which the fake sources should be added 

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

1081 Photometric calibration to be used to calibrate the fake sources 

1082 

1083 Yields 

1084 ------- 

1085 starImages : `generator` 

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

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

1088 

1089 Notes 

1090 ----- 

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

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

1093 given calibration radius used in the photometric calibration step. 

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

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

1096 """ 

1097 

1098 self.log.info("Making %d fake star 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 starIm = psf.computeImage(xy) 

1108 starIm /= correctedFlux 

1109 

1110 except InvalidParameterError: 

1111 self.log.info("Star 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 starIm *= flux 

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

1121 

1122 def cleanCat(self, fakeCat, starCheckVal): 

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

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

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

1126 

1127 Parameters 

1128 ---------- 

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

1130 The catalog of fake sources to be input 

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

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

1133 

1134 Returns 

1135 ------- 

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

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

1138 """ 

1139 

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

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

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

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

1144 fakeCat = fakeCat[rowsToKeep] 

1145 

1146 minN = galsim.Sersic._minimum_n 

1147 maxN = galsim.Sersic._maximum_n 

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

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

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

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

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

1153 numRowsNotUsed, minN, maxN) 

1154 fakeCat = fakeCat[rowsWithGoodSersic] 

1155 

1156 if self.config.doSubSelectSources: 

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

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

1159 fakeCat = fakeCat[fakeCat['select']] 

1160 

1161 return fakeCat 

1162 

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

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

1165 

1166 Parameters 

1167 ---------- 

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

1169 The image into which the fake sources should be added 

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

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

1172 and the locations they are to be inserted at. 

1173 sourceType : `str` 

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

1175 

1176 Returns 

1177 ------- 

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

1179 

1180 Notes 

1181 ----- 

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

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

1184 """ 

1185 

1186 imageBBox = image.getBBox() 

1187 imageMI = image.maskedImage 

1188 

1189 for (fakeImage, xy) in fakeImages: 

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

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

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

1193 if sourceType == "galaxy": 

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

1195 else: 

1196 interpFakeImage = fakeImage 

1197 

1198 interpFakeImBBox = interpFakeImage.getBBox() 

1199 interpFakeImBBox.clip(imageBBox) 

1200 

1201 if interpFakeImBBox.getArea() > 0: 

1202 imageMIView = imageMI[interpFakeImBBox] 

1203 clippedFakeImage = interpFakeImage[interpFakeImBBox] 

1204 clippedFakeImageMI = afwImage.MaskedImageF(clippedFakeImage) 

1205 clippedFakeImageMI.mask.set(self.bitmask) 

1206 imageMIView += clippedFakeImageMI 

1207 

1208 return image 

1209 

1210 def _getMetadataName(self): 

1211 """Disable metadata writing""" 

1212 return None