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

406 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-12-14 04:32 -0800

1# This file is part of pipe_tasks. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

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

7# for details of code ownership. 

8# 

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

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

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

12# (at your option) any later version. 

13# 

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

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

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

17# GNU General Public License for more details. 

18# 

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

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

21 

22""" 

23Insert fakes into deepCoadds 

24""" 

25 

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

27 

28import galsim 

29import numpy as np 

30from astropy import units as u 

31 

32import lsst.geom as geom 

33import lsst.afw.image as afwImage 

34import lsst.afw.math as afwMath 

35import lsst.pex.config as pexConfig 

36import lsst.pipe.base as pipeBase 

37 

38from lsst.pipe.base import PipelineTask, PipelineTaskConfig, PipelineTaskConnections 

39import lsst.pipe.base.connectionTypes as cT 

40from lsst.pex.exceptions import LogicError, InvalidParameterError 

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

42 

43 

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

45 """Add fake sources to the given exposure 

46 

47 Parameters 

48 ---------- 

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

50 The exposure into which the fake sources should be added 

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

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

53 surface brightness profiles to inject. 

54 calibFluxRadius : `float`, optional 

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

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

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

58 slot_CalibFlux_instFlux. 

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

60 Logger. 

61 """ 

62 exposure.mask.addMaskPlane("FAKE") 

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

64 if logger: 

65 logger.info(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 pixScale = wcs.getPixelScale(bbox.getCenter()).asArcseconds() 

75 

76 for spt, gsObj in objects: 

77 pt = wcs.skyToPixel(spt) 

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

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

80 if logger: 

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

82 

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

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

85 

86 # This check is here because sometimes the WCS 

87 # is multivalued and objects that should not be 

88 # were being included. 

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

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

91 continue 

92 

93 try: 

94 psfArr = psf.computeKernelImage(pt).array 

95 apCorr = psf.computeApertureFlux(calibFluxRadius, pt) 

96 except InvalidParameterError: 

97 # Try mapping to nearest point contained in bbox. 

98 contained_pt = Point2D( 

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

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

101 ) 

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

103 if logger: 

104 logger.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 apCorr = psf.computeApertureFlux(calibFluxRadius, contained_pt) 

113 except InvalidParameterError: 

114 if logger: 

115 logger.infof( 

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

117 pt 

118 ) 

119 continue 

120 

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

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 runQuantum(self, butlerQC, inputRefs, outputRefs): 

567 inputs = butlerQC.get(inputRefs) 

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

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

570 

571 outputs = self.run(**inputs) 

572 butlerQC.put(outputs, outputRefs) 

573 

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

575 """Add fake sources to an image. 

576 

577 Parameters 

578 ---------- 

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

580 The catalog of fake sources to be input 

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

582 The image into which the fake sources should be added 

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

584 WCS to use to add fake sources 

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

586 Photometric calibration to be used to calibrate the fake sources 

587 

588 Returns 

589 ------- 

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

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

592 

593 Notes 

594 ----- 

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

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

597 

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

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

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

601 the image and the image with fakes included returned. 

602 

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

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

605 """ 

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

607 # so we can reset at the end. 

608 origWcs = image.getWcs() 

609 origPhotoCalib = image.getPhotoCalib() 

610 image.setWcs(wcs) 

611 image.setPhotoCalib(photoCalib) 

612 

613 band = image.getFilter().bandLabel 

614 fakeCat = self._standardizeColumns(fakeCat, band) 

615 

616 fakeCat = self.addPixCoords(fakeCat, image) 

617 fakeCat = self.trimFakeCat(fakeCat, image) 

618 

619 if len(fakeCat) > 0: 

620 if not self.config.insertImages: 

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

622 galCheckVal = "galaxy" 

623 starCheckVal = "star" 

624 trailCheckVal = "trail" 

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

626 galCheckVal = b"galaxy" 

627 starCheckVal = b"star" 

628 trailCheckVal = b"trail" 

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

630 galCheckVal = 1 

631 starCheckVal = 0 

632 trailCheckVal = 2 

633 else: 

634 raise TypeError( 

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

636 ) 

637 if self.config.doCleanCat: 

638 fakeCat = self.cleanCat(fakeCat, starCheckVal) 

639 

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

641 trailCheckVal) 

642 else: 

643 generator = self._generateGSObjectsFromImages(image, fakeCat) 

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

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

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

647 image.mask.addMaskPlane("FAKE") 

648 else: 

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

650 

651 # restore original exposure WCS and photoCalib 

652 image.setWcs(origWcs) 

653 image.setPhotoCalib(origPhotoCalib) 

654 

655 resultStruct = pipeBase.Struct(imageWithFakes=image) 

656 

657 return resultStruct 

658 

659 def _standardizeColumns(self, fakeCat, band): 

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

661 names in the input catalog. 

662 

663 Parameters 

664 ---------- 

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

666 The catalog of fake sources to be input 

667 band : `str` 

668 Label for the current band being processed. 

669 

670 Returns 

671 ------- 

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

673 The standardized catalog of fake sources 

674 """ 

675 cfg = self.config 

676 replace_dict = {} 

677 

678 def add_to_replace_dict(new_name, depr_name, std_name): 

679 if new_name in fakeCat.columns: 

680 replace_dict[new_name] = std_name 

681 elif depr_name in fakeCat.columns: 

682 replace_dict[depr_name] = std_name 

683 else: 

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

685 

686 # Prefer new config variables over deprecated config variables. 

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

688 for new_name, depr_name, std_name in [ 

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

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

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

692 ]: 

693 add_to_replace_dict(new_name, depr_name, std_name) 

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

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

696 for new_name, depr_name, std_name in [ 

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

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

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

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

701 ]: 

702 add_to_replace_dict(new_name, depr_name, std_name) 

703 

704 if cfg.doSubSelectSources: 

705 add_to_replace_dict( 

706 cfg.select_col, 

707 cfg.sourceSelectionColName, 

708 'select' 

709 ) 

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

711 

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

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

714 # Just handle these manually. 

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

716 if ( 

717 cfg.bulge_semimajor_col in fakeCat.columns 

718 and cfg.bulge_axis_ratio_col in fakeCat.columns 

719 ): 

720 fakeCat = fakeCat.rename( 

721 columns={ 

722 cfg.bulge_semimajor_col: 'bulge_semimajor', 

723 cfg.bulge_axis_ratio_col: 'bulge_axis_ratio', 

724 cfg.disk_semimajor_col: 'disk_semimajor', 

725 cfg.disk_axis_ratio_col: 'disk_axis_ratio', 

726 }, 

727 copy=False 

728 ) 

729 elif ( 

730 cfg.bulgeHLR in fakeCat.columns 

731 and cfg.aBulge in fakeCat.columns 

732 and cfg.bBulge in fakeCat.columns 

733 ): 

734 fakeCat['bulge_axis_ratio'] = ( 

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

736 ) 

737 fakeCat['bulge_semimajor'] = ( 

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

739 ) 

740 fakeCat['disk_axis_ratio'] = ( 

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

742 ) 

743 fakeCat['disk_semimajor'] = ( 

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

745 ) 

746 else: 

747 raise ValueError( 

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

749 "axis ratio." 

750 ) 

751 

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

753 if cfg.bulge_disk_flux_ratio_col in fakeCat.columns: 

754 fakeCat = fakeCat.rename( 

755 columns={ 

756 cfg.bulge_disk_flux_ratio_col: 'bulge_disk_flux_ratio' 

757 }, 

758 copy=False 

759 ) 

760 else: 

761 fakeCat['bulge_disk_flux_ratio'] = 1.0 

762 

763 return fakeCat 

764 

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

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

767 

768 Parameters 

769 ---------- 

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

771 The exposure into which the fake sources should be added 

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

773 The catalog of fake sources to be input 

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

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

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

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

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

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

780 

781 Yields 

782 ------ 

783 gsObjects : `generator` 

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

785 """ 

786 wcs = exposure.getWcs() 

787 photoCalib = exposure.getPhotoCalib() 

788 

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

790 

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

792 ra = row['ra'] 

793 dec = row['dec'] 

794 skyCoord = SpherePoint(ra, dec, radians) 

795 xy = wcs.skyToPixel(skyCoord) 

796 

797 try: 

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

799 except LogicError: 

800 continue 

801 

802 sourceType = row[self.config.sourceType] 

803 if sourceType == galCheckVal: 

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

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

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

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

808 

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

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

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

812 

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

814 gal = gal.withFlux(flux) 

815 

816 yield skyCoord, gal 

817 elif sourceType == starCheckVal: 

818 star = galsim.DeltaFunction() 

819 star = star.withFlux(flux) 

820 yield skyCoord, star 

821 elif sourceType == trailCheckVal: 

822 length = row['trail_length'] 

823 angle = row['trail_angle'] 

824 

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

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

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

828 trail = galsim.Box(length, thickness) 

829 trail = trail.rotate(theta) 

830 trail = trail.withFlux(flux*length) 

831 

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

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

834 # coordinates, we must transform the trail here. 

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

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

837 

838 yield skyCoord, trail 

839 else: 

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

841 

842 def _generateGSObjectsFromImages(self, exposure, fakeCat): 

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

844 

845 Parameters 

846 ---------- 

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

848 The exposure into which the fake sources should be added 

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

850 The catalog of fake sources to be input 

851 

852 Yields 

853 ------ 

854 gsObjects : `generator` 

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

856 """ 

857 band = exposure.getFilter().bandLabel 

858 wcs = exposure.getWcs() 

859 photoCalib = exposure.getPhotoCalib() 

860 

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

862 

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

864 ra = row['ra'] 

865 dec = row['dec'] 

866 skyCoord = SpherePoint(ra, dec, radians) 

867 xy = wcs.skyToPixel(skyCoord) 

868 

869 try: 

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

871 except LogicError: 

872 continue 

873 

874 imFile = row[band+"imFilename"] 

875 try: 

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

877 except AttributeError: 

878 pass 

879 imFile = imFile.strip() 

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

881 

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

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

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

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

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

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

888 # exception. 

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

890 raise RuntimeError( 

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

892 ) 

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

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

895 # position. 

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

897 mat = linWcs.getMatrix() 

898 im.wcs = galsim.JacobianWCS( 

899 mat[0, 0], mat[0, 1], mat[1, 0], mat[1, 1] 

900 ) 

901 else: 

902 raise ValueError( 

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

904 ) 

905 

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

907 obj = obj.withFlux(flux) 

908 yield skyCoord, obj 

909 

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

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

912 

913 Parameters 

914 ---------- 

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

916 The catalog of fake sources to be input 

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

918 WCS to use to add fake sources 

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

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

921 The PSF information to use to make the PSF images 

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

923 Photometric calibration to be used to calibrate the fake sources 

924 band : `str` 

925 The filter band that the observation was taken in. 

926 pixelScale : `float` 

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

928 

929 Returns 

930 ------- 

931 galImages : `list` 

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

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

934 For sources labelled as galaxy. 

935 starImages : `list` 

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

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

938 For sources labelled as star. 

939 

940 Notes 

941 ----- 

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

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

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

945 """ 

946 galImages = [] 

947 starImages = [] 

948 

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

950 

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

952 fakeCat["sourceType"].array, 

953 fakeCat['mag'].array, 

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

955 

956 im = afwImage.ImageF.readFits(imFile) 

957 

958 xy = geom.Point2D(x, y) 

959 

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

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

962 try: 

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

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

965 psfKernel /= correctedFlux 

966 

967 except InvalidParameterError: 

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

969 continue 

970 

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

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

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

974 

975 try: 

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

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

978 continue 

979 

980 imSum = np.sum(outIm) 

981 divIm = outIm/imSum 

982 

983 try: 

984 flux = photoCalib.magnitudeToInstFlux(mag, xy) 

985 except LogicError: 

986 flux = 0 

987 

988 imWithFlux = flux*divIm 

989 

990 if sourceType == b"galaxy": 

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

992 if sourceType == b"star": 

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

994 

995 return galImages, starImages 

996 

997 def addPixCoords(self, fakeCat, image): 

998 

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

1000 

1001 Parameters 

1002 ---------- 

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

1004 The catalog of fake sources to be input 

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

1006 The image into which the fake sources should be added 

1007 

1008 Returns 

1009 ------- 

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

1011 """ 

1012 wcs = image.getWcs() 

1013 ras = fakeCat['ra'].values 

1014 decs = fakeCat['dec'].values 

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

1016 fakeCat["x"] = xs 

1017 fakeCat["y"] = ys 

1018 

1019 return fakeCat 

1020 

1021 def trimFakeCat(self, fakeCat, image): 

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

1023 

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

1025 

1026 Parameters 

1027 ---------- 

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

1029 The catalog of fake sources to be input 

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

1031 The image into which the fake sources should be added 

1032 

1033 Returns 

1034 ------- 

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

1036 The original fakeCat trimmed to the area of the image 

1037 """ 

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

1039 

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

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

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

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

1044 

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

1046 

1047 # also filter on the image BBox in pixel space 

1048 xs = fakeCat["x"].values 

1049 ys = fakeCat["y"].values 

1050 

1051 isContainedXy = xs >= wideBbox.minX 

1052 isContainedXy &= xs <= wideBbox.maxX 

1053 isContainedXy &= ys >= wideBbox.minY 

1054 isContainedXy &= ys <= wideBbox.maxY 

1055 

1056 return fakeCat[isContainedRaDec & isContainedXy] 

1057 

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

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

1060 

1061 Parameters 

1062 ---------- 

1063 band : `str` 

1064 pixelScale : `float` 

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

1066 The PSF information to use to make the PSF images 

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

1068 The catalog of fake sources to be input 

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

1070 Photometric calibration to be used to calibrate the fake sources 

1071 

1072 Yields 

1073 ------ 

1074 galImages : `generator` 

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

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

1077 

1078 Notes 

1079 ----- 

1080 

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

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

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

1084 

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

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

1087 attached to the config options. 

1088 

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

1090 """ 

1091 

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

1093 

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

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

1096 

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

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

1099 try: 

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

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

1102 psfKernel /= correctedFlux 

1103 

1104 except InvalidParameterError: 

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

1106 continue 

1107 

1108 try: 

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

1110 except LogicError: 

1111 flux = 0 

1112 

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

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

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

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

1117 

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

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

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

1121 

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

1123 gal = gal.withFlux(flux) 

1124 

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

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

1127 try: 

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

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

1130 continue 

1131 

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

1133 

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

1135 

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

1137 

1138 Parameters 

1139 ---------- 

1140 band : `str` 

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

1142 The PSF information to use to make the PSF images 

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

1144 The catalog of fake sources to be input 

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

1146 The image into which the fake sources should be added 

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

1148 Photometric calibration to be used to calibrate the fake sources 

1149 

1150 Yields 

1151 ------ 

1152 starImages : `generator` 

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

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

1155 

1156 Notes 

1157 ----- 

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

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

1160 given calibration radius used in the photometric calibration step. 

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

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

1163 """ 

1164 

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

1166 

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

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

1169 

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

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

1172 try: 

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

1174 starIm = psf.computeImage(xy) 

1175 starIm /= correctedFlux 

1176 

1177 except InvalidParameterError: 

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

1179 continue 

1180 

1181 try: 

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

1183 except LogicError: 

1184 flux = 0 

1185 

1186 starIm *= flux 

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

1188 

1189 def cleanCat(self, fakeCat, starCheckVal): 

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

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

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

1193 

1194 Parameters 

1195 ---------- 

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

1197 The catalog of fake sources to be input 

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

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

1200 

1201 Returns 

1202 ------- 

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

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

1205 """ 

1206 

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

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

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

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

1211 fakeCat = fakeCat[rowsToKeep] 

1212 

1213 minN = galsim.Sersic._minimum_n 

1214 maxN = galsim.Sersic._maximum_n 

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

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

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

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

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

1220 numRowsNotUsed, minN, maxN) 

1221 fakeCat = fakeCat[rowsWithGoodSersic] 

1222 

1223 if self.config.doSubSelectSources: 

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

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

1226 fakeCat = fakeCat[fakeCat['select']] 

1227 

1228 return fakeCat 

1229 

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

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

1232 

1233 Parameters 

1234 ---------- 

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

1236 The image into which the fake sources should be added 

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

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

1239 and the locations they are to be inserted at. 

1240 sourceType : `str` 

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

1242 

1243 Returns 

1244 ------- 

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

1246 

1247 Notes 

1248 ----- 

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

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

1251 """ 

1252 

1253 imageBBox = image.getBBox() 

1254 imageMI = image.maskedImage 

1255 

1256 for (fakeImage, xy) in fakeImages: 

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

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

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

1260 if sourceType == "galaxy": 

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

1262 else: 

1263 interpFakeImage = fakeImage 

1264 

1265 interpFakeImBBox = interpFakeImage.getBBox() 

1266 interpFakeImBBox.clip(imageBBox) 

1267 

1268 if interpFakeImBBox.getArea() > 0: 

1269 imageMIView = imageMI[interpFakeImBBox] 

1270 clippedFakeImage = interpFakeImage[interpFakeImBBox] 

1271 clippedFakeImageMI = afwImage.MaskedImageF(clippedFakeImage) 

1272 clippedFakeImageMI.mask.set(self.bitmask) 

1273 imageMIView += clippedFakeImageMI 

1274 

1275 return image