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

425 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-07-19 05:39 -0700

1# This file is part of pipe tasks 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (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 

28from astropy import units as u 

29 

30import lsst.geom as geom 

31import lsst.afw.image as afwImage 

32import lsst.afw.math as afwMath 

33import lsst.pex.config as pexConfig 

34import lsst.pipe.base as pipeBase 

35 

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

37import lsst.pipe.base.connectionTypes as cT 

38from lsst.pex.exceptions import LogicError, InvalidParameterError 

39from lsst.coadd.utils.coaddDataIdContainer import ExistingCoaddDataIdContainer 

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

41 

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

43 

44 

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

46 """Add fake sources to the given exposure 

47 

48 Parameters 

49 ---------- 

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

51 The exposure into which the fake sources should be added 

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

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

54 surface brightness profiles to inject. 

55 calibFluxRadius : `float`, optional 

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

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

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

59 slot_CalibFlux_instFlux. 

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

61 Logger. 

62 """ 

63 exposure.mask.addMaskPlane("FAKE") 

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

65 if logger: 

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

67 

68 wcs = exposure.getWcs() 

69 psf = exposure.getPsf() 

70 

71 bbox = exposure.getBBox() 

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

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

74 

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

76 

77 for spt, gsObj in objects: 

78 pt = wcs.skyToPixel(spt) 

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

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

81 if logger: 

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

83 

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

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

86 

87 # This check is here because sometimes the WCS 

88 # is multivalued and objects that should not be 

89 # were being included. 

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

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

92 continue 

93 

94 try: 

95 psfArr = psf.computeKernelImage(pt).array 

96 except InvalidParameterError: 

97 # Try mapping to nearest point contained in bbox. 

98 contained_pt = Point2D( 

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

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

101 ) 

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

103 if logger: 

104 logger.infof( 

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

106 pt 

107 ) 

108 continue 

109 # otherwise, try again with new point 

110 try: 

111 psfArr = psf.computeKernelImage(contained_pt).array 

112 except InvalidParameterError: 

113 if logger: 

114 logger.infof( 

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

116 pt 

117 ) 

118 continue 

119 

120 apCorr = psf.computeApertureFlux(calibFluxRadius) 

121 psfArr /= apCorr 

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

123 

124 conv = galsim.Convolve(gsObj, gsPSF) 

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

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

127 subBounds &= fullBounds 

128 

129 if subBounds.area() > 0: 

130 subImg = gsImg[subBounds] 

131 offset = posd - subBounds.true_center 

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

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

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

135 

136 conv.drawImage( 

137 subImg, 

138 add_to_image=True, 

139 offset=offset, 

140 wcs=gsWCS, 

141 method='no_pixel' 

142 ) 

143 

144 subBox = geom.Box2I( 

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

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

147 ) 

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

149 

150 

151def _isWCSGalsimDefault(wcs, hdr): 

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

153 or if it's just the galsim default. 

154 

155 Parameters 

156 ---------- 

157 wcs : galsim.BaseWCS 

158 Potentially default WCS. 

159 hdr : galsim.fits.FitsHeader 

160 Header as read in by galsim. 

161 

162 Returns 

163 ------- 

164 isDefault : bool 

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

166 """ 

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

168 return False 

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

170 return False 

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

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

173 for wcs_type in galsim.fitswcs.fits_wcs_types: 

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

175 try: 

176 wcs_type._readHeader(hdr) 

177 return False 

178 except Exception: 

179 pass 

180 else: 

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

182 

183 

184class InsertFakesConnections(PipelineTaskConnections, 

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

186 "fakesType": "fakes_"}, 

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

188 

189 image = cT.Input( 

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

191 name="{coaddName}Coadd", 

192 storageClass="ExposureF", 

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

194 ) 

195 

196 fakeCat = cT.Input( 

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

198 name="{fakesType}fakeSourceCat", 

199 storageClass="DataFrame", 

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

201 ) 

202 

203 imageWithFakes = cT.Output( 

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

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

206 storageClass="ExposureF", 

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

208 ) 

209 

210 

211class InsertFakesConfig(PipelineTaskConfig, 

212 pipelineConnections=InsertFakesConnections): 

213 """Config for inserting fake sources 

214 """ 

215 

216 # Unchanged 

217 

218 doCleanCat = pexConfig.Field( 

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

220 dtype=bool, 

221 default=True, 

222 ) 

223 

224 fakeType = pexConfig.Field( 

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

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

227 "catalog.", 

228 dtype=str, 

229 default="static", 

230 ) 

231 

232 calibFluxRadius = pexConfig.Field( 

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

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

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

236 dtype=float, 

237 default=12.0, 

238 ) 

239 

240 coaddName = pexConfig.Field( 

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

242 dtype=str, 

243 default="deep", 

244 ) 

245 

246 doSubSelectSources = pexConfig.Field( 

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

248 "set in the sourceSelectionColName config option.", 

249 dtype=bool, 

250 default=False 

251 ) 

252 

253 insertImages = pexConfig.Field( 

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

255 dtype=bool, 

256 default=False, 

257 ) 

258 

259 insertOnlyStars = pexConfig.Field( 

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

261 dtype=bool, 

262 default=False, 

263 ) 

264 

265 doProcessAllDataIds = pexConfig.Field( 

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

267 dtype=bool, 

268 default=False, 

269 ) 

270 

271 trimBuffer = pexConfig.Field( 

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

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

274 dtype=int, 

275 default=100, 

276 ) 

277 

278 sourceType = pexConfig.Field( 

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

280 dtype=str, 

281 default="sourceType", 

282 ) 

283 

284 fits_alignment = pexConfig.ChoiceField( 

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

286 dtype=str, 

287 allowed={ 

288 "wcs": ( 

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

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

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

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

293 "image." 

294 ), 

295 "pixel": ( 

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

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

298 "distances in the target image." 

299 ) 

300 }, 

301 default="pixel" 

302 ) 

303 

304 # New source catalog config variables 

305 

306 ra_col = pexConfig.Field( 

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

308 dtype=str, 

309 default="ra", 

310 ) 

311 

312 dec_col = pexConfig.Field( 

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

314 dtype=str, 

315 default="dec", 

316 ) 

317 

318 bulge_semimajor_col = pexConfig.Field( 

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

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

321 dtype=str, 

322 default="bulge_semimajor", 

323 ) 

324 

325 bulge_axis_ratio_col = pexConfig.Field( 

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

327 "half-light ellipse.", 

328 dtype=str, 

329 default="bulge_axis_ratio", 

330 ) 

331 

332 bulge_pa_col = pexConfig.Field( 

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

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

335 "half-light ellipse.", 

336 dtype=str, 

337 default="bulge_pa", 

338 ) 

339 

340 bulge_n_col = pexConfig.Field( 

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

342 dtype=str, 

343 default="bulge_n", 

344 ) 

345 

346 disk_semimajor_col = pexConfig.Field( 

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

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

349 dtype=str, 

350 default="disk_semimajor", 

351 ) 

352 

353 disk_axis_ratio_col = pexConfig.Field( 

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

355 "half-light ellipse.", 

356 dtype=str, 

357 default="disk_axis_ratio", 

358 ) 

359 

360 disk_pa_col = pexConfig.Field( 

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

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

363 "half-light ellipse.", 

364 dtype=str, 

365 default="disk_pa", 

366 ) 

367 

368 disk_n_col = pexConfig.Field( 

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

370 dtype=str, 

371 default="disk_n", 

372 ) 

373 

374 bulge_disk_flux_ratio_col = pexConfig.Field( 

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

376 dtype=str, 

377 default="bulge_disk_flux_ratio", 

378 ) 

379 

380 mag_col = pexConfig.Field( 

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

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

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

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

385 dtype=str, 

386 default="%s_mag" 

387 ) 

388 

389 select_col = pexConfig.Field( 

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

391 "add.", 

392 dtype=str, 

393 default="select", 

394 ) 

395 

396 length_col = pexConfig.Field( 

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

398 dtype=str, 

399 default="trail_length", 

400 ) 

401 

402 angle_col = pexConfig.Field( 

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

404 dtype=str, 

405 default="trail_angle", 

406 ) 

407 

408 # Deprecated config variables 

409 

410 raColName = pexConfig.Field( 

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

412 dtype=str, 

413 default="raJ2000", 

414 deprecated="Use `ra_col` instead." 

415 ) 

416 

417 decColName = pexConfig.Field( 

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

419 dtype=str, 

420 default="decJ2000", 

421 deprecated="Use `dec_col` instead." 

422 ) 

423 

424 diskHLR = pexConfig.Field( 

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

426 dtype=str, 

427 default="DiskHalfLightRadius", 

428 deprecated=( 

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

430 " to specify disk half-light ellipse." 

431 ) 

432 ) 

433 

434 aDisk = pexConfig.Field( 

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

436 "catalog.", 

437 dtype=str, 

438 default="a_d", 

439 deprecated=( 

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

441 " to specify disk half-light ellipse." 

442 ) 

443 ) 

444 

445 bDisk = pexConfig.Field( 

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

447 dtype=str, 

448 default="b_d", 

449 deprecated=( 

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

451 " to specify disk half-light ellipse." 

452 ) 

453 ) 

454 

455 paDisk = pexConfig.Field( 

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

457 dtype=str, 

458 default="pa_disk", 

459 deprecated=( 

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

461 " to specify disk half-light ellipse." 

462 ) 

463 ) 

464 

465 nDisk = pexConfig.Field( 

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

467 dtype=str, 

468 default="disk_n", 

469 deprecated="Use `disk_n_col` instead." 

470 ) 

471 

472 bulgeHLR = pexConfig.Field( 

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

474 dtype=str, 

475 default="BulgeHalfLightRadius", 

476 deprecated=( 

477 "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and " 

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

479 ) 

480 ) 

481 

482 aBulge = pexConfig.Field( 

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

484 dtype=str, 

485 default="a_b", 

486 deprecated=( 

487 "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and " 

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

489 ) 

490 ) 

491 

492 bBulge = pexConfig.Field( 

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

494 "catalog.", 

495 dtype=str, 

496 default="b_b", 

497 deprecated=( 

498 "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and " 

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

500 ) 

501 ) 

502 

503 paBulge = pexConfig.Field( 

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

505 dtype=str, 

506 default="pa_bulge", 

507 deprecated=( 

508 "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and " 

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

510 ) 

511 ) 

512 

513 nBulge = pexConfig.Field( 

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

515 dtype=str, 

516 default="bulge_n", 

517 deprecated="Use `bulge_n_col` instead." 

518 ) 

519 

520 magVar = pexConfig.Field( 

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

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

523 dtype=str, 

524 default="%smagVar", 

525 deprecated="Use `mag_col` instead." 

526 ) 

527 

528 sourceSelectionColName = pexConfig.Field( 

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

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

531 dtype=str, 

532 default="templateSource", 

533 deprecated="Use `select_col` instead." 

534 ) 

535 

536 

537class InsertFakesTask(PipelineTask, CmdLineTask): 

538 """Insert fake objects into images. 

539 

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

541 from the specified file and then modelled using galsim. 

542 

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

544 image. 

545 

546 `addPixCoords` 

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

548 `mkFakeGalsimGalaxies` 

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

550 `mkFakeStars` 

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

552 input file. 

553 `cleanCat` 

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

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

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

557 to only those which are True in this column. 

558 `addFakeSources` 

559 Add the fake sources to the image. 

560 

561 """ 

562 

563 _DefaultName = "insertFakes" 

564 ConfigClass = InsertFakesConfig 

565 

566 def runDataRef(self, dataRef): 

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

568 

569 Parameters 

570 ---------- 

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

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

573 Used to access the following data products: 

574 deepCoadd 

575 """ 

576 

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

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

579 

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

581 

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

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

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

585 # task structure for ref cats is in place. 

586 self.fakeSourceCatType = "deepCoadd_fakeSourceCat" 

587 else: 

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

589 

590 coadd = dataRef.get("deepCoadd") 

591 wcs = coadd.getWcs() 

592 photoCalib = coadd.getPhotoCalib() 

593 

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

595 

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

597 

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

599 inputs = butlerQC.get(inputRefs) 

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

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

602 

603 outputs = self.run(**inputs) 

604 butlerQC.put(outputs, outputRefs) 

605 

606 @classmethod 

607 def _makeArgumentParser(cls): 

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

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

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

611 ContainerClass=ExistingCoaddDataIdContainer) 

612 return parser 

613 

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

615 """Add fake sources to an image. 

616 

617 Parameters 

618 ---------- 

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

620 The catalog of fake sources to be input 

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

622 The image into which the fake sources should be added 

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

624 WCS to use to add fake sources 

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

626 Photometric calibration to be used to calibrate the fake sources 

627 

628 Returns 

629 ------- 

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

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

632 

633 Notes 

634 ----- 

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

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

637 

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

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

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

641 the image and the image with fakes included returned. 

642 

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

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

645 """ 

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

647 # so we can reset at the end. 

648 origWcs = image.getWcs() 

649 origPhotoCalib = image.getPhotoCalib() 

650 image.setWcs(wcs) 

651 image.setPhotoCalib(photoCalib) 

652 

653 band = image.getFilter().bandLabel 

654 fakeCat = self._standardizeColumns(fakeCat, band) 

655 

656 fakeCat = self.addPixCoords(fakeCat, image) 

657 fakeCat = self.trimFakeCat(fakeCat, image) 

658 

659 if len(fakeCat) > 0: 

660 if not self.config.insertImages: 

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

662 galCheckVal = "galaxy" 

663 starCheckVal = "star" 

664 trailCheckVal = "trail" 

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

666 galCheckVal = b"galaxy" 

667 starCheckVal = b"star" 

668 trailCheckVal = b"trail" 

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

670 galCheckVal = 1 

671 starCheckVal = 0 

672 trailCheckVal = 2 

673 else: 

674 raise TypeError( 

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

676 ) 

677 if self.config.doCleanCat: 

678 fakeCat = self.cleanCat(fakeCat, starCheckVal) 

679 

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

681 trailCheckVal) 

682 else: 

683 generator = self._generateGSObjectsFromImages(image, fakeCat) 

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

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

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

687 image.mask.addMaskPlane("FAKE") 

688 else: 

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

690 

691 # restore original exposure WCS and photoCalib 

692 image.setWcs(origWcs) 

693 image.setPhotoCalib(origPhotoCalib) 

694 

695 resultStruct = pipeBase.Struct(imageWithFakes=image) 

696 

697 return resultStruct 

698 

699 def _standardizeColumns(self, fakeCat, band): 

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

701 names in the input catalog. 

702 

703 Parameters 

704 ---------- 

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

706 The catalog of fake sources to be input 

707 band : `str` 

708 Label for the current band being processed. 

709 

710 Returns 

711 ------- 

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

713 The standardized catalog of fake sources 

714 """ 

715 cfg = self.config 

716 replace_dict = {} 

717 

718 def add_to_replace_dict(new_name, depr_name, std_name): 

719 if new_name in fakeCat.columns: 

720 replace_dict[new_name] = std_name 

721 elif depr_name in fakeCat.columns: 

722 replace_dict[depr_name] = std_name 

723 else: 

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

725 

726 # Prefer new config variables over deprecated config variables. 

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

728 for new_name, depr_name, std_name in [ 

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

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

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

732 ]: 

733 add_to_replace_dict(new_name, depr_name, std_name) 

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

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

736 for new_name, depr_name, std_name in [ 

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

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

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

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

741 ]: 

742 add_to_replace_dict(new_name, depr_name, std_name) 

743 

744 if cfg.doSubSelectSources: 

745 add_to_replace_dict( 

746 cfg.select_col, 

747 cfg.sourceSelectionColName, 

748 'select' 

749 ) 

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

751 

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

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

754 # Just handle these manually. 

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

756 if ( 

757 cfg.bulge_semimajor_col in fakeCat.columns 

758 and cfg.bulge_axis_ratio_col in fakeCat.columns 

759 ): 

760 fakeCat = fakeCat.rename( 

761 columns={ 

762 cfg.bulge_semimajor_col: 'bulge_semimajor', 

763 cfg.bulge_axis_ratio_col: 'bulge_axis_ratio', 

764 cfg.disk_semimajor_col: 'disk_semimajor', 

765 cfg.disk_axis_ratio_col: 'disk_axis_ratio', 

766 }, 

767 copy=False 

768 ) 

769 elif ( 

770 cfg.bulgeHLR in fakeCat.columns 

771 and cfg.aBulge in fakeCat.columns 

772 and cfg.bBulge in fakeCat.columns 

773 ): 

774 fakeCat['bulge_axis_ratio'] = ( 

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

776 ) 

777 fakeCat['bulge_semimajor'] = ( 

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

779 ) 

780 fakeCat['disk_axis_ratio'] = ( 

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

782 ) 

783 fakeCat['disk_semimajor'] = ( 

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

785 ) 

786 else: 

787 raise ValueError( 

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

789 "axis ratio." 

790 ) 

791 

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

793 if cfg.bulge_disk_flux_ratio_col in fakeCat.columns: 

794 fakeCat = fakeCat.rename( 

795 columns={ 

796 cfg.bulge_disk_flux_ratio_col: 'bulge_disk_flux_ratio' 

797 }, 

798 copy=False 

799 ) 

800 else: 

801 fakeCat['bulge_disk_flux_ratio'] = 1.0 

802 

803 return fakeCat 

804 

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

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

807 

808 Parameters 

809 ---------- 

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

811 The exposure into which the fake sources should be added 

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

813 The catalog of fake sources to be input 

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

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

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

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

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

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

820 

821 Yields 

822 ------ 

823 gsObjects : `generator` 

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

825 """ 

826 wcs = exposure.getWcs() 

827 photoCalib = exposure.getPhotoCalib() 

828 

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

830 

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

832 ra = row['ra'] 

833 dec = row['dec'] 

834 skyCoord = SpherePoint(ra, dec, radians) 

835 xy = wcs.skyToPixel(skyCoord) 

836 

837 try: 

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

839 except LogicError: 

840 continue 

841 

842 sourceType = row[self.config.sourceType] 

843 if sourceType == galCheckVal: 

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

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

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

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

848 

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

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

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

852 

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

854 gal = gal.withFlux(flux) 

855 

856 yield skyCoord, gal 

857 elif sourceType == starCheckVal: 

858 star = galsim.DeltaFunction() 

859 star = star.withFlux(flux) 

860 yield skyCoord, star 

861 elif sourceType == trailCheckVal: 

862 length = row['trail_length'] 

863 angle = row['trail_angle'] 

864 

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

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

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

868 trail = galsim.Box(length, thickness) 

869 trail = trail.rotate(theta) 

870 trail = trail.withFlux(flux*length) 

871 

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

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

874 # coordinates, we must transform the trail here. 

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

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

877 

878 yield skyCoord, trail 

879 else: 

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

881 

882 def _generateGSObjectsFromImages(self, exposure, fakeCat): 

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

884 

885 Parameters 

886 ---------- 

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

888 The exposure into which the fake sources should be added 

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

890 The catalog of fake sources to be input 

891 

892 Yields 

893 ------ 

894 gsObjects : `generator` 

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

896 """ 

897 band = exposure.getFilter().bandLabel 

898 wcs = exposure.getWcs() 

899 photoCalib = exposure.getPhotoCalib() 

900 

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

902 

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

904 ra = row['ra'] 

905 dec = row['dec'] 

906 skyCoord = SpherePoint(ra, dec, radians) 

907 xy = wcs.skyToPixel(skyCoord) 

908 

909 try: 

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

911 except LogicError: 

912 continue 

913 

914 imFile = row[band+"imFilename"] 

915 try: 

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

917 except AttributeError: 

918 pass 

919 imFile = imFile.strip() 

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

921 

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

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

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

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

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

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

928 # exception. 

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

930 raise RuntimeError( 

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

932 ) 

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

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

935 # position. 

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

937 mat = linWcs.getMatrix() 

938 im.wcs = galsim.JacobianWCS( 

939 mat[0, 0], mat[0, 1], mat[1, 0], mat[1, 1] 

940 ) 

941 else: 

942 raise ValueError( 

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

944 ) 

945 

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

947 obj = obj.withFlux(flux) 

948 yield skyCoord, obj 

949 

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

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

952 

953 Parameters 

954 ---------- 

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

956 The catalog of fake sources to be input 

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

958 WCS to use to add fake sources 

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

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

961 The PSF information to use to make the PSF images 

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

963 Photometric calibration to be used to calibrate the fake sources 

964 band : `str` 

965 The filter band that the observation was taken in. 

966 pixelScale : `float` 

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

968 

969 Returns 

970 ------- 

971 galImages : `list` 

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

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

974 For sources labelled as galaxy. 

975 starImages : `list` 

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

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

978 For sources labelled as star. 

979 

980 Notes 

981 ----- 

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

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

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

985 """ 

986 galImages = [] 

987 starImages = [] 

988 

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

990 

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

992 fakeCat["sourceType"].array, 

993 fakeCat['mag'].array, 

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

995 

996 im = afwImage.ImageF.readFits(imFile) 

997 

998 xy = geom.Point2D(x, y) 

999 

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

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

1002 try: 

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

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

1005 psfKernel /= correctedFlux 

1006 

1007 except InvalidParameterError: 

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

1009 continue 

1010 

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

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

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

1014 

1015 try: 

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

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

1018 continue 

1019 

1020 imSum = np.sum(outIm) 

1021 divIm = outIm/imSum 

1022 

1023 try: 

1024 flux = photoCalib.magnitudeToInstFlux(mag, xy) 

1025 except LogicError: 

1026 flux = 0 

1027 

1028 imWithFlux = flux*divIm 

1029 

1030 if sourceType == b"galaxy": 

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

1032 if sourceType == b"star": 

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

1034 

1035 return galImages, starImages 

1036 

1037 def addPixCoords(self, fakeCat, image): 

1038 

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

1040 

1041 Parameters 

1042 ---------- 

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

1044 The catalog of fake sources to be input 

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

1046 The image into which the fake sources should be added 

1047 

1048 Returns 

1049 ------- 

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

1051 """ 

1052 wcs = image.getWcs() 

1053 ras = fakeCat['ra'].values 

1054 decs = fakeCat['dec'].values 

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

1056 fakeCat["x"] = xs 

1057 fakeCat["y"] = ys 

1058 

1059 return fakeCat 

1060 

1061 def trimFakeCat(self, fakeCat, image): 

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

1063 

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

1065 

1066 Parameters 

1067 ---------- 

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

1069 The catalog of fake sources to be input 

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

1071 The image into which the fake sources should be added 

1072 

1073 Returns 

1074 ------- 

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

1076 The original fakeCat trimmed to the area of the image 

1077 """ 

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

1079 

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

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

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

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

1084 

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

1086 

1087 # also filter on the image BBox in pixel space 

1088 xs = fakeCat["x"].values 

1089 ys = fakeCat["y"].values 

1090 

1091 isContainedXy = xs >= wideBbox.minX 

1092 isContainedXy &= xs <= wideBbox.maxX 

1093 isContainedXy &= ys >= wideBbox.minY 

1094 isContainedXy &= ys <= wideBbox.maxY 

1095 

1096 return fakeCat[isContainedRaDec & isContainedXy] 

1097 

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

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

1100 

1101 Parameters 

1102 ---------- 

1103 band : `str` 

1104 pixelScale : `float` 

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

1106 The PSF information to use to make the PSF images 

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

1108 The catalog of fake sources to be input 

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

1110 Photometric calibration to be used to calibrate the fake sources 

1111 

1112 Yields 

1113 ------- 

1114 galImages : `generator` 

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

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

1117 

1118 Notes 

1119 ----- 

1120 

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

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

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

1124 

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

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

1127 attached to the config options. 

1128 

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

1130 """ 

1131 

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

1133 

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

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

1136 

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

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

1139 try: 

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

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

1142 psfKernel /= correctedFlux 

1143 

1144 except InvalidParameterError: 

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

1146 continue 

1147 

1148 try: 

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

1150 except LogicError: 

1151 flux = 0 

1152 

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

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

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

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

1157 

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

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

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

1161 

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

1163 gal = gal.withFlux(flux) 

1164 

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

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

1167 try: 

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

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

1170 continue 

1171 

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

1173 

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

1175 

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

1177 

1178 Parameters 

1179 ---------- 

1180 band : `str` 

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

1182 The PSF information to use to make the PSF images 

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

1184 The catalog of fake sources to be input 

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

1186 The image into which the fake sources should be added 

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

1188 Photometric calibration to be used to calibrate the fake sources 

1189 

1190 Yields 

1191 ------- 

1192 starImages : `generator` 

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

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

1195 

1196 Notes 

1197 ----- 

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

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

1200 given calibration radius used in the photometric calibration step. 

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

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

1203 """ 

1204 

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

1206 

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

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

1209 

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

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

1212 try: 

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

1214 starIm = psf.computeImage(xy) 

1215 starIm /= correctedFlux 

1216 

1217 except InvalidParameterError: 

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

1219 continue 

1220 

1221 try: 

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

1223 except LogicError: 

1224 flux = 0 

1225 

1226 starIm *= flux 

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

1228 

1229 def cleanCat(self, fakeCat, starCheckVal): 

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

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

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

1233 

1234 Parameters 

1235 ---------- 

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

1237 The catalog of fake sources to be input 

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

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

1240 

1241 Returns 

1242 ------- 

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

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

1245 """ 

1246 

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

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

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

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

1251 fakeCat = fakeCat[rowsToKeep] 

1252 

1253 minN = galsim.Sersic._minimum_n 

1254 maxN = galsim.Sersic._maximum_n 

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

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

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

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

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

1260 numRowsNotUsed, minN, maxN) 

1261 fakeCat = fakeCat[rowsWithGoodSersic] 

1262 

1263 if self.config.doSubSelectSources: 

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

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

1266 fakeCat = fakeCat[fakeCat['select']] 

1267 

1268 return fakeCat 

1269 

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

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

1272 

1273 Parameters 

1274 ---------- 

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

1276 The image into which the fake sources should be added 

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

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

1279 and the locations they are to be inserted at. 

1280 sourceType : `str` 

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

1282 

1283 Returns 

1284 ------- 

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

1286 

1287 Notes 

1288 ----- 

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

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

1291 """ 

1292 

1293 imageBBox = image.getBBox() 

1294 imageMI = image.maskedImage 

1295 

1296 for (fakeImage, xy) in fakeImages: 

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

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

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

1300 if sourceType == "galaxy": 

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

1302 else: 

1303 interpFakeImage = fakeImage 

1304 

1305 interpFakeImBBox = interpFakeImage.getBBox() 

1306 interpFakeImBBox.clip(imageBBox) 

1307 

1308 if interpFakeImBBox.getArea() > 0: 

1309 imageMIView = imageMI[interpFakeImBBox] 

1310 clippedFakeImage = interpFakeImage[interpFakeImBBox] 

1311 clippedFakeImageMI = afwImage.MaskedImageF(clippedFakeImage) 

1312 clippedFakeImageMI.mask.set(self.bitmask) 

1313 imageMIView += clippedFakeImageMI 

1314 

1315 return image 

1316 

1317 def _getMetadataName(self): 

1318 """Disable metadata writing""" 

1319 return None