Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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 fits_alignment = pexConfig.ChoiceField( 

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

268 dtype=str, 

269 allowed={ 

270 "wcs": ( 

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

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

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

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

275 "image." 

276 ), 

277 "pixel": ( 

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

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

280 "distances in the target image." 

281 ) 

282 }, 

283 default="pixel" 

284 ) 

285 

286 # New source catalog config variables 

287 

288 ra_col = pexConfig.Field( 

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

290 dtype=str, 

291 default="ra", 

292 ) 

293 

294 dec_col = pexConfig.Field( 

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

296 dtype=str, 

297 default="dec", 

298 ) 

299 

300 bulge_semimajor_col = pexConfig.Field( 

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

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

303 dtype=str, 

304 default="bulge_semimajor", 

305 ) 

306 

307 bulge_axis_ratio_col = pexConfig.Field( 

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

309 "half-light ellipse.", 

310 dtype=str, 

311 default="bulge_axis_ratio", 

312 ) 

313 

314 bulge_pa_col = pexConfig.Field( 

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

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

317 "half-light ellipse.", 

318 dtype=str, 

319 default="bulge_pa", 

320 ) 

321 

322 bulge_n_col = pexConfig.Field( 

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

324 dtype=str, 

325 default="bulge_n", 

326 ) 

327 

328 disk_semimajor_col = pexConfig.Field( 

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

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

331 dtype=str, 

332 default="disk_semimajor", 

333 ) 

334 

335 disk_axis_ratio_col = pexConfig.Field( 

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

337 "half-light ellipse.", 

338 dtype=str, 

339 default="disk_axis_ratio", 

340 ) 

341 

342 disk_pa_col = pexConfig.Field( 

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

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

345 "half-light ellipse.", 

346 dtype=str, 

347 default="disk_pa", 

348 ) 

349 

350 disk_n_col = pexConfig.Field( 

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

352 dtype=str, 

353 default="disk_n", 

354 ) 

355 

356 bulge_disk_flux_ratio_col = pexConfig.Field( 

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

358 dtype=str, 

359 default="bulge_disk_flux_ratio", 

360 ) 

361 

362 mag_col = pexConfig.Field( 

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

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

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

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

367 dtype=str, 

368 default="%s_mag" 

369 ) 

370 

371 select_col = pexConfig.Field( 

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

373 "add.", 

374 dtype=str, 

375 default="select", 

376 ) 

377 

378 # Deprecated config variables 

379 

380 raColName = pexConfig.Field( 

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

382 dtype=str, 

383 default="raJ2000", 

384 deprecated="Use `ra_col` instead." 

385 ) 

386 

387 decColName = pexConfig.Field( 

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

389 dtype=str, 

390 default="decJ2000", 

391 deprecated="Use `dec_col` instead." 

392 ) 

393 

394 diskHLR = pexConfig.Field( 

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

396 dtype=str, 

397 default="DiskHalfLightRadius", 

398 deprecated=( 

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

400 " to specify disk half-light ellipse." 

401 ) 

402 ) 

403 

404 aDisk = pexConfig.Field( 

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

406 "catalog.", 

407 dtype=str, 

408 default="a_d", 

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

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

417 dtype=str, 

418 default="b_d", 

419 deprecated=( 

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

421 " to specify disk half-light ellipse." 

422 ) 

423 ) 

424 

425 paDisk = pexConfig.Field( 

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

427 dtype=str, 

428 default="pa_disk", 

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

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

437 dtype=str, 

438 default="disk_n", 

439 deprecated="Use `disk_n` instead." 

440 ) 

441 

442 bulgeHLR = pexConfig.Field( 

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

444 dtype=str, 

445 default="BulgeHalfLightRadius", 

446 deprecated=( 

447 "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and " 

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

449 ) 

450 ) 

451 

452 aBulge = pexConfig.Field( 

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

454 dtype=str, 

455 default="a_b", 

456 deprecated=( 

457 "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and " 

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

459 ) 

460 ) 

461 

462 bBulge = pexConfig.Field( 

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

464 "catalog.", 

465 dtype=str, 

466 default="b_b", 

467 deprecated=( 

468 "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and " 

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

470 ) 

471 ) 

472 

473 paBulge = pexConfig.Field( 

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

475 dtype=str, 

476 default="pa_bulge", 

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

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

485 dtype=str, 

486 default="bulge_n", 

487 deprecated="Use `bulge_n` instead." 

488 ) 

489 

490 magVar = pexConfig.Field( 

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

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

493 dtype=str, 

494 default="%smagVar", 

495 deprecated="Use `mag_col` instead." 

496 ) 

497 

498 sourceSelectionColName = pexConfig.Field( 

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

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

501 dtype=str, 

502 default="templateSource", 

503 deprecated="Use `select_col` instead." 

504 ) 

505 

506 

507class InsertFakesTask(PipelineTask, CmdLineTask): 

508 """Insert fake objects into images. 

509 

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

511 from the specified file and then modelled using galsim. 

512 

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

514 image. 

515 

516 `addPixCoords` 

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

518 `mkFakeGalsimGalaxies` 

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

520 `mkFakeStars` 

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

522 input file. 

523 `cleanCat` 

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

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

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

527 to only those which are True in this column. 

528 `addFakeSources` 

529 Add the fake sources to the image. 

530 

531 """ 

532 

533 _DefaultName = "insertFakes" 

534 ConfigClass = InsertFakesConfig 

535 

536 def runDataRef(self, dataRef): 

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

538 

539 Parameters 

540 ---------- 

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

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

543 Used to access the following data products: 

544 deepCoadd 

545 """ 

546 

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

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

549 

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

551 

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

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

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

555 # task structure for ref cats is in place. 

556 self.fakeSourceCatType = "deepCoadd_fakeSourceCat" 

557 else: 

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

559 

560 coadd = dataRef.get("deepCoadd") 

561 wcs = coadd.getWcs() 

562 photoCalib = coadd.getPhotoCalib() 

563 

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

565 

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

567 

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

569 inputs = butlerQC.get(inputRefs) 

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

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

572 

573 outputs = self.run(**inputs) 

574 butlerQC.put(outputs, outputRefs) 

575 

576 @classmethod 

577 def _makeArgumentParser(cls): 

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

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

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

581 ContainerClass=ExistingCoaddDataIdContainer) 

582 return parser 

583 

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

585 """Add fake sources to an image. 

586 

587 Parameters 

588 ---------- 

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

590 The catalog of fake sources to be input 

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

592 The image into which the fake sources should be added 

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

594 WCS to use to add fake sources 

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

596 Photometric calibration to be used to calibrate the fake sources 

597 

598 Returns 

599 ------- 

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

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

602 

603 Notes 

604 ----- 

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

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

607 

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

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

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

611 the image and the image with fakes included returned. 

612 

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

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

615 """ 

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

617 # so we can reset at the end. 

618 origWcs = image.getWcs() 

619 origPhotoCalib = image.getPhotoCalib() 

620 image.setWcs(wcs) 

621 image.setPhotoCalib(photoCalib) 

622 

623 band = image.getFilterLabel().bandLabel 

624 fakeCat = self._standardizeColumns(fakeCat, band) 

625 

626 fakeCat = self.addPixCoords(fakeCat, image) 

627 fakeCat = self.trimFakeCat(fakeCat, image) 

628 

629 if len(fakeCat) > 0: 

630 if not self.config.insertImages: 

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

632 galCheckVal = "galaxy" 

633 starCheckVal = "star" 

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

635 galCheckVal = b"galaxy" 

636 starCheckVal = b"star" 

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

638 galCheckVal = 1 

639 starCheckVal = 0 

640 else: 

641 raise TypeError( 

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

643 ) 

644 if self.config.doCleanCat: 

645 fakeCat = self.cleanCat(fakeCat, starCheckVal) 

646 

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

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: 

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: 

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): 

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 specifiy an object is a galaxy. 

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

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

784 

785 Yields 

786 ------ 

787 gsObjects : `generator` 

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

789 """ 

790 wcs = exposure.getWcs() 

791 photoCalib = exposure.getPhotoCalib() 

792 

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

794 

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

796 ra = row['ra'] 

797 dec = row['dec'] 

798 skyCoord = SpherePoint(ra, dec, radians) 

799 xy = wcs.skyToPixel(skyCoord) 

800 

801 try: 

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

803 except LogicError: 

804 continue 

805 

806 sourceType = row[self.config.sourceType] 

807 if sourceType == galCheckVal: 

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

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

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

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

812 

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

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

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

816 

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

818 gal = gal.withFlux(flux) 

819 

820 yield skyCoord, gal 

821 elif sourceType == starCheckVal: 

822 star = galsim.DeltaFunction() 

823 star = star.withFlux(flux) 

824 yield skyCoord, star 

825 else: 

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

827 

828 def _generateGSObjectsFromImages(self, exposure, fakeCat): 

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

830 

831 Parameters 

832 ---------- 

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

834 The exposure into which the fake sources should be added 

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

836 The catalog of fake sources to be input 

837 

838 Yields 

839 ------ 

840 gsObjects : `generator` 

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

842 """ 

843 band = exposure.getFilterLabel().bandLabel 

844 wcs = exposure.getWcs() 

845 photoCalib = exposure.getPhotoCalib() 

846 

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

848 

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

850 ra = row['ra'] 

851 dec = row['dec'] 

852 skyCoord = SpherePoint(ra, dec, radians) 

853 xy = wcs.skyToPixel(skyCoord) 

854 

855 try: 

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

857 except LogicError: 

858 continue 

859 

860 imFile = row[band+"imFilename"] 

861 try: 

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

863 except AttributeError: 

864 pass 

865 imFile = imFile.strip() 

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

867 

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

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

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

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

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

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

874 # exception. 

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

876 raise RuntimeError( 

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

878 ) 

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

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

881 # position. 

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

883 mat = linWcs.getMatrix() 

884 im.wcs = galsim.JacobianWCS( 

885 mat[0, 0], mat[0, 1], mat[1, 0], mat[1, 1] 

886 ) 

887 else: 

888 raise ValueError( 

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

890 ) 

891 

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

893 obj = obj.withFlux(flux) 

894 yield skyCoord, obj 

895 

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

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

898 

899 Parameters 

900 ---------- 

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

902 The catalog of fake sources to be input 

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

904 WCS to use to add fake sources 

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

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

907 The PSF information to use to make the PSF images 

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

909 Photometric calibration to be used to calibrate the fake sources 

910 band : `str` 

911 The filter band that the observation was taken in. 

912 pixelScale : `float` 

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

914 

915 Returns 

916 ------- 

917 galImages : `list` 

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

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

920 For sources labelled as galaxy. 

921 starImages : `list` 

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

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

924 For sources labelled as star. 

925 

926 Notes 

927 ----- 

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

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

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

931 """ 

932 galImages = [] 

933 starImages = [] 

934 

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

936 

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

938 fakeCat["sourceType"].array, 

939 fakeCat['mag'].array, 

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

941 

942 im = afwImage.ImageF.readFits(imFile) 

943 

944 xy = geom.Point2D(x, y) 

945 

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

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

948 try: 

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

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

951 psfKernel /= correctedFlux 

952 

953 except InvalidParameterError: 

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

955 continue 

956 

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

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

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

960 

961 try: 

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

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

964 continue 

965 

966 imSum = np.sum(outIm) 

967 divIm = outIm/imSum 

968 

969 try: 

970 flux = photoCalib.magnitudeToInstFlux(mag, xy) 

971 except LogicError: 

972 flux = 0 

973 

974 imWithFlux = flux*divIm 

975 

976 if sourceType == b"galaxy": 

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

978 if sourceType == b"star": 

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

980 

981 return galImages, starImages 

982 

983 def addPixCoords(self, fakeCat, image): 

984 

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

986 

987 Parameters 

988 ---------- 

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

990 The catalog of fake sources to be input 

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

992 The image into which the fake sources should be added 

993 

994 Returns 

995 ------- 

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

997 """ 

998 wcs = image.getWcs() 

999 ras = fakeCat['ra'].values 

1000 decs = fakeCat['dec'].values 

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

1002 fakeCat["x"] = xs 

1003 fakeCat["y"] = ys 

1004 

1005 return fakeCat 

1006 

1007 def trimFakeCat(self, fakeCat, image): 

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

1009 

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

1011 

1012 Parameters 

1013 ---------- 

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

1015 The catalog of fake sources to be input 

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

1017 The image into which the fake sources should be added 

1018 

1019 Returns 

1020 ------- 

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

1022 The original fakeCat trimmed to the area of the image 

1023 """ 

1024 

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

1026 xs = fakeCat["x"].values 

1027 ys = fakeCat["y"].values 

1028 

1029 isContained = xs >= bbox.minX 

1030 isContained &= xs <= bbox.maxX 

1031 isContained &= ys >= bbox.minY 

1032 isContained &= ys <= bbox.maxY 

1033 

1034 return fakeCat[isContained] 

1035 

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

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

1038 

1039 Parameters 

1040 ---------- 

1041 band : `str` 

1042 pixelScale : `float` 

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

1044 The PSF information to use to make the PSF images 

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

1046 The catalog of fake sources to be input 

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

1048 Photometric calibration to be used to calibrate the fake sources 

1049 

1050 Yields 

1051 ------- 

1052 galImages : `generator` 

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

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

1055 

1056 Notes 

1057 ----- 

1058 

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

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

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

1062 

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

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

1065 attached to the config options. 

1066 

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

1068 """ 

1069 

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

1071 

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

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

1074 

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

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

1077 try: 

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

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

1080 psfKernel /= correctedFlux 

1081 

1082 except InvalidParameterError: 

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

1084 continue 

1085 

1086 try: 

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

1088 except LogicError: 

1089 flux = 0 

1090 

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

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

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

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

1095 

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

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

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

1099 

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

1101 gal = gal.withFlux(flux) 

1102 

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

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

1105 try: 

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

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

1108 continue 

1109 

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

1111 

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

1113 

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

1115 

1116 Parameters 

1117 ---------- 

1118 band : `str` 

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

1120 The PSF information to use to make the PSF images 

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

1122 The catalog of fake sources to be input 

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

1124 The image into which the fake sources should be added 

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

1126 Photometric calibration to be used to calibrate the fake sources 

1127 

1128 Yields 

1129 ------- 

1130 starImages : `generator` 

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

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

1133 

1134 Notes 

1135 ----- 

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

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

1138 given calibration radius used in the photometric calibration step. 

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

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

1141 """ 

1142 

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

1144 

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

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

1147 

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

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

1150 try: 

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

1152 starIm = psf.computeImage(xy) 

1153 starIm /= correctedFlux 

1154 

1155 except InvalidParameterError: 

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

1157 continue 

1158 

1159 try: 

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

1161 except LogicError: 

1162 flux = 0 

1163 

1164 starIm *= flux 

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

1166 

1167 def cleanCat(self, fakeCat, starCheckVal): 

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

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

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

1171 

1172 Parameters 

1173 ---------- 

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

1175 The catalog of fake sources to be input 

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

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

1178 

1179 Returns 

1180 ------- 

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

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

1183 """ 

1184 

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

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

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

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

1189 fakeCat = fakeCat[rowsToKeep] 

1190 

1191 minN = galsim.Sersic._minimum_n 

1192 maxN = galsim.Sersic._maximum_n 

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

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

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

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

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

1198 numRowsNotUsed, minN, maxN) 

1199 fakeCat = fakeCat[rowsWithGoodSersic] 

1200 

1201 if self.config.doSubSelectSources: 

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

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

1204 fakeCat = fakeCat[fakeCat['select']] 

1205 

1206 return fakeCat 

1207 

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

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

1210 

1211 Parameters 

1212 ---------- 

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

1214 The image into which the fake sources should be added 

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

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

1217 and the locations they are to be inserted at. 

1218 sourceType : `str` 

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

1220 

1221 Returns 

1222 ------- 

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

1224 

1225 Notes 

1226 ----- 

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

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

1229 """ 

1230 

1231 imageBBox = image.getBBox() 

1232 imageMI = image.maskedImage 

1233 

1234 for (fakeImage, xy) in fakeImages: 

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

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

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

1238 if sourceType == "galaxy": 

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

1240 else: 

1241 interpFakeImage = fakeImage 

1242 

1243 interpFakeImBBox = interpFakeImage.getBBox() 

1244 interpFakeImBBox.clip(imageBBox) 

1245 

1246 if interpFakeImBBox.getArea() > 0: 

1247 imageMIView = imageMI[interpFakeImBBox] 

1248 clippedFakeImage = interpFakeImage[interpFakeImBBox] 

1249 clippedFakeImageMI = afwImage.MaskedImageF(clippedFakeImage) 

1250 clippedFakeImageMI.mask.set(self.bitmask) 

1251 imageMIView += clippedFakeImageMI 

1252 

1253 return image 

1254 

1255 def _getMetadataName(self): 

1256 """Disable metadata writing""" 

1257 return None