23 Insert fakes into deepCoadds
26 from astropy.table
import Table
31 import lsst.pex.config
as pexConfig
34 from lsst.pipe.base import CmdLineTask, PipelineTask, PipelineTaskConfig, PipelineTaskConnections
38 from lsst.geom import SpherePoint, radians, Box2D
41 __all__ = [
"InsertFakesConfig",
"InsertFakesTask"]
45 dimensions=(
"tract",
"patch",
"abstract_filter",
"skymap")):
48 doc=
"Image into which fakes are to be added.",
49 name=
"{CoaddName}Coadd",
50 storageClass=
"ExposureF",
51 dimensions=(
"tract",
"patch",
"abstract_filter",
"skymap")
55 doc=
"Catalog of fake sources to draw inputs from.",
56 name=
"{CoaddName}Coadd_fakeSourceCat",
57 storageClass=
"Parquet",
58 dimensions=(
"tract",
"skymap")
61 imageWithFakes = cT.Output(
62 doc=
"Image with fake sources added.",
63 name=
"fakes_{CoaddName}Coadd",
64 storageClass=
"ExposureF",
65 dimensions=(
"tract",
"patch",
"abstract_filter",
"skymap")
69 class InsertFakesConfig(PipelineTaskConfig,
70 pipelineConnections=InsertFakesConnections):
71 """Config for inserting fake sources
75 The default column names are those from the University of Washington sims database.
78 raColName = pexConfig.Field(
79 doc=
"RA column name used in the fake source catalog.",
84 decColName = pexConfig.Field(
85 doc=
"Dec. column name used in the fake source catalog.",
90 doCleanCat = pexConfig.Field(
91 doc=
"If true removes bad sources from the catalog.",
96 diskHLR = pexConfig.Field(
97 doc=
"Column name for the disk half light radius used in the fake source catalog.",
99 default=
"DiskHalfLightRadius",
102 bulgeHLR = pexConfig.Field(
103 doc=
"Column name for the bulge half light radius used in the fake source catalog.",
105 default=
"BulgeHalfLightRadius",
108 magVar = pexConfig.Field(
109 doc=
"The column name for the magnitude calculated taking variability into account. In the format "
110 "``filter name``magVar, e.g. imagVar for the magnitude in the i band.",
115 nDisk = pexConfig.Field(
116 doc=
"The column name for the sersic index of the disk component used in the fake source catalog.",
121 nBulge = pexConfig.Field(
122 doc=
"The column name for the sersic index of the bulge component used in the fake source catalog.",
127 aDisk = pexConfig.Field(
128 doc=
"The column name for the semi major axis length of the disk component used in the fake source"
134 aBulge = pexConfig.Field(
135 doc=
"The column name for the semi major axis length of the bulge component.",
140 bDisk = pexConfig.Field(
141 doc=
"The column name for the semi minor axis length of the disk component.",
146 bBulge = pexConfig.Field(
147 doc=
"The column name for the semi minor axis length of the bulge component used in the fake source "
153 paDisk = pexConfig.Field(
154 doc=
"The column name for the PA of the disk component used in the fake source catalog.",
159 paBulge = pexConfig.Field(
160 doc=
"The column name for the PA of the bulge component used in the fake source catalog.",
165 sourceType = pexConfig.Field(
166 doc=
"The column name for the source type used in the fake source catalog.",
168 default=
"sourceType",
171 fakeType = pexConfig.Field(
172 doc=
"What type of fake catalog to use, snapshot (includes variability in the magnitudes calculated "
173 "from the MJD of the image), static (no variability) or filename for a user defined fits"
179 calibFluxRadius = pexConfig.Field(
180 doc=
"Aperture radius (in pixels) that was used to define the calibration for this image+catalog. "
181 "This will be used to produce the correct instrumental fluxes within the radius. "
182 "This value should match that of the field defined in slot_CalibFlux_instFlux.",
187 coaddName = pexConfig.Field(
188 doc=
"The name of the type of coadd used",
193 doSubSelectSources = pexConfig.Field(
194 doc=
"Set to True if you wish to sub select sources to be input based on the value in the column"
195 "set in the sourceSelectionColName config option.",
200 sourceSelectionColName = pexConfig.Field(
201 doc=
"The name of the column in the input fakes catalogue to be used to determine which sources to"
202 "add, default is none and when this is used all sources are added.",
204 default=
"templateSource"
208 class InsertFakesTask(PipelineTask, CmdLineTask):
209 """Insert fake objects into images.
211 Add fake stars and galaxies to the given image, read in through the dataRef. Galaxy parameters are read in
212 from the specified file and then modelled using galsim.
214 `InsertFakesTask` has five functions that make images of the fake sources and then add them to the
218 Use the WCS information to add the pixel coordinates of each source.
219 `mkFakeGalsimGalaxies`
220 Use Galsim to make fake double sersic galaxies for each set of galaxy parameters in the input file.
222 Use the PSF information from the image to make a fake star using the magnitude information from the
225 Remove rows of the input fake catalog which have half light radius, of either the bulge or the disk,
226 that are 0. Also removes rows that have Sersic index outside of galsim's allowed paramters. If
227 the config option sourceSelectionColName is set then this function limits the catalog of input fakes
228 to only those which are True in this column.
230 Add the fake sources to the image.
234 _DefaultName =
"insertFakes"
235 ConfigClass = InsertFakesConfig
237 def runDataRef(self, dataRef):
238 """Read in/write out the required data products and add fake sources to the deepCoadd.
242 dataRef : `lsst.daf.persistence.butlerSubset.ButlerDataRef`
243 Data reference defining the image to have fakes added to it
244 Used to access the following data products:
248 infoStr =
"Adding fakes to: tract: %d, patch: %s, filter: %s" % (dataRef.dataId[
"tract"],
249 dataRef.dataId[
"patch"],
250 dataRef.dataId[
"filter"])
251 self.log.info(infoStr)
255 if self.config.fakeType ==
"static":
256 fakeCat = dataRef.get(
"deepCoadd_fakeSourceCat").toDataFrame()
259 self.fakeSourceCatType =
"deepCoadd_fakeSourceCat"
261 fakeCat = Table.read(self.config.fakeType).to_pandas()
263 coadd = dataRef.get(
"deepCoadd")
265 photoCalib = coadd.getPhotoCalib()
267 imageWithFakes = self.run(fakeCat, coadd, wcs, photoCalib)
269 dataRef.put(imageWithFakes.imageWithFakes,
"fakes_deepCoadd")
271 def runQuantum(self, butlerQC, inputRefs, outputRefs):
272 inputs = butlerQC.get(inputRefs)
273 inputs[
"wcs"] = inputs[
"image"].getWcs()
274 inputs[
"photoCalib"] = inputs[
"image"].getPhotoCalib()
276 outputs = self.run(**inputs)
277 butlerQC.put(outputs, outputRefs)
280 def _makeArgumentParser(cls):
281 parser = pipeBase.ArgumentParser(name=cls._DefaultName)
282 parser.add_id_argument(name=
"--id", datasetType=
"deepCoadd",
283 help=
"data IDs for the deepCoadd, e.g. --id tract=12345 patch=1,2 filter=r",
284 ContainerClass=ExistingCoaddDataIdContainer)
287 def run(self, fakeCat, image, wcs, photoCalib):
288 """Add fake sources to an image.
292 fakeCat : `pandas.core.frame.DataFrame`
293 The catalog of fake sources to be input
294 image : `lsst.afw.image.exposure.exposure.ExposureF`
295 The image into which the fake sources should be added
296 wcs : `lsst.afw.geom.SkyWcs`
297 WCS to use to add fake sources
298 photoCalib : `lsst.afw.image.photoCalib.PhotoCalib`
299 Photometric calibration to be used to calibrate the fake sources
303 resultStruct : `lsst.pipe.base.struct.Struct`
304 contains : image : `lsst.afw.image.exposure.exposure.ExposureF`
308 Adds pixel coordinates for each source to the fakeCat and removes objects with bulge or disk half
309 light radius = 0 (if ``config.doCleanCat = True``).
311 Adds the ``Fake`` mask plane to the image which is then set by `addFakeSources` to mark where fake
312 sources have been added. Uses the information in the ``fakeCat`` to make fake galaxies (using galsim)
313 and fake stars, using the PSF models from the PSF information for the image. These are then added to
314 the image and the image with fakes included returned.
316 The galsim galaxies are made using a double sersic profile, one for the bulge and one for the disk,
317 this is then convolved with the PSF at that point.
320 image.mask.addMaskPlane(
"FAKE")
321 self.bitmask = image.mask.getPlaneBitMask(
"FAKE")
322 self.log.info(
"Adding mask plane with bitmask %d" % self.bitmask)
324 fakeCat = self.addPixCoords(fakeCat, wcs)
325 if self.config.doCleanCat:
326 fakeCat = self.cleanCat(fakeCat)
327 fakeCat = self.trimFakeCat(fakeCat, image, wcs)
329 band = image.getFilter().getName()
330 pixelScale = wcs.getPixelScale().asArcseconds()
333 if isinstance(fakeCat[self.config.sourceType].iloc[0], str):
334 galCheckVal =
"galaxy"
335 starCheckVal =
"star"
336 elif isinstance(fakeCat[self.config.sourceType].iloc[0], bytes):
337 galCheckVal = b
"galaxy"
338 starCheckVal = b
"star"
339 elif isinstance(fakeCat[self.config.sourceType].iloc[0], (int, float)):
343 raise TypeError(
"sourceType column does not have required type, should be str, bytes or int")
345 galaxies = (fakeCat[self.config.sourceType] == galCheckVal)
346 galImages = self.mkFakeGalsimGalaxies(fakeCat[galaxies], band, photoCalib, pixelScale, psf, image)
347 image = self.addFakeSources(image, galImages,
"galaxy")
349 stars = (fakeCat[self.config.sourceType] == starCheckVal)
350 starImages = self.mkFakeStars(fakeCat[stars], band, photoCalib, psf, image)
351 image = self.addFakeSources(image, starImages,
"star")
352 resultStruct = pipeBase.Struct(imageWithFakes=image)
356 def addPixCoords(self, fakeCat, wcs):
358 """Add pixel coordinates to the catalog of fakes.
362 fakeCat : `pandas.core.frame.DataFrame`
363 The catalog of fake sources to be input
364 wcs : `lsst.afw.geom.SkyWcs`
365 WCS to use to add fake sources
369 fakeCat : `pandas.core.frame.DataFrame`
373 The default option is to use the WCS information from the image. If the ``useUpdatedCalibs`` config
374 option is set then it will use the updated WCS from jointCal.
377 ras = fakeCat[self.config.raColName].values
378 decs = fakeCat[self.config.decColName].values
379 skyCoords = [
SpherePoint(ra, dec, radians)
for (ra, dec)
in zip(ras, decs)]
380 pixCoords = wcs.skyToPixel(skyCoords)
381 xs = [coord.getX()
for coord
in pixCoords]
382 ys = [coord.getY()
for coord
in pixCoords]
388 def trimFakeCat(self, fakeCat, image, wcs):
389 """Trim the fake cat to about the size of the input image.
393 fakeCat : `pandas.core.frame.DataFrame`
394 The catalog of fake sources to be input
395 image : `lsst.afw.image.exposure.exposure.ExposureF`
396 The image into which the fake sources should be added
397 wcs : `lsst.afw.geom.SkyWcs`
398 WCS to use to add fake sources
402 fakeCat : `pandas.core.frame.DataFrame`
403 The original fakeCat trimmed to the area of the image
406 bbox =
Box2D(image.getBBox())
407 corners = bbox.getCorners()
409 skyCorners = wcs.pixelToSky(corners)
413 coord =
SpherePoint(row[self.config.raColName], row[self.config.decColName], radians)
414 return region.contains(coord.getVector())
416 return fakeCat[fakeCat.apply(trim, axis=1)]
418 def mkFakeGalsimGalaxies(self, fakeCat, band, photoCalib, pixelScale, psf, image):
419 """Make images of fake galaxies using GalSim.
425 psf : `lsst.meas.extensions.psfex.psfexPsf.PsfexPsf`
426 The PSF information to use to make the PSF images
427 fakeCat : `pandas.core.frame.DataFrame`
428 The catalog of fake sources to be input
429 photoCalib : `lsst.afw.image.photoCalib.PhotoCalib`
430 Photometric calibration to be used to calibrate the fake sources
434 galImages : `generator`
435 A generator of tuples of `lsst.afw.image.exposure.exposure.ExposureF` and
436 `lsst.geom.Point2D` of their locations.
441 Fake galaxies are made by combining two sersic profiles, one for the bulge and one for the disk. Each
442 component has an individual sersic index (n), a, b and position angle (PA). The combined profile is
443 then convolved with the PSF at the specified x, y position on the image.
445 The names of the columns in the ``fakeCat`` are configurable and are the column names from the
446 University of Washington simulations database as default. For more information see the doc strings
447 attached to the config options.
449 See mkFakeStars doc string for an explanation of calibration to instrumental flux.
452 self.log.info(
"Making %d fake galaxy images" % len(fakeCat))
454 for (index, row)
in fakeCat.iterrows():
458 correctedFlux = psf.computeApertureFlux(self.config.calibFluxRadius, xy)
459 psfKernel = psf.computeKernelImage(xy).getArray()
460 psfKernel /= correctedFlux
462 except InvalidParameterError:
463 self.log.info(
"Galaxy at %0.4f, %0.4f outside of image" % (row[
"x"], row[
"y"]))
467 flux = photoCalib.magnitudeToInstFlux(row[self.config.magVar % band], xy)
471 bulge = galsim.Sersic(row[self.config.nBulge], half_light_radius=row[self.config.bulgeHLR])
472 axisRatioBulge = row[self.config.bBulge]/row[self.config.aBulge]
473 bulge = bulge.shear(q=axisRatioBulge, beta=((90 - row[self.config.paBulge])*galsim.degrees))
475 disk = galsim.Sersic(row[self.config.nDisk], half_light_radius=row[self.config.diskHLR])
476 axisRatioDisk = row[self.config.bDisk]/row[self.config.aDisk]
477 disk = disk.shear(q=axisRatioDisk, beta=((90 - row[self.config.paDisk])*galsim.degrees))
480 gal = gal.withFlux(flux)
482 psfIm = galsim.InterpolatedImage(galsim.Image(psfKernel), scale=pixelScale)
483 gal = galsim.Convolve([gal, psfIm])
485 galIm = gal.drawImage(scale=pixelScale, method=
"real_space").array
486 except (galsim.errors.GalSimFFTSizeError, MemoryError):
489 yield (afwImage.ImageF(galIm), xy)
491 def mkFakeStars(self, fakeCat, band, photoCalib, psf, image):
493 """Make fake stars based off the properties in the fakeCat.
498 psf : `lsst.meas.extensions.psfex.psfexPsf.PsfexPsf`
499 The PSF information to use to make the PSF images
500 fakeCat : `pandas.core.frame.DataFrame`
501 The catalog of fake sources to be input
502 image : `lsst.afw.image.exposure.exposure.ExposureF`
503 The image into which the fake sources should be added
504 photoCalib : `lsst.afw.image.photoCalib.PhotoCalib`
505 Photometric calibration to be used to calibrate the fake sources
509 starImages : `generator`
510 A generator of tuples of `lsst.afw.image.ImageF` of fake stars and
511 `lsst.geom.Point2D` of their locations.
515 To take a given magnitude and translate to the number of counts in the image
516 we use photoCalib.magnitudeToInstFlux, which returns the instrumental flux for the
517 given calibration radius used in the photometric calibration step.
518 Thus `calibFluxRadius` should be set to this same radius so that we can normalize
519 the PSF model to the correct instrumental flux within calibFluxRadius.
522 self.log.info(
"Making %d fake star images" % len(fakeCat))
524 for (index, row)
in fakeCat.iterrows():
530 correctedFlux = psf.computeApertureFlux(self.config.calibFluxRadius, xy)
531 starIm = psf.computeImage(xy)
532 starIm /= correctedFlux
534 except InvalidParameterError:
535 self.log.info(
"Star at %0.4f, %0.4f outside of image" % (row[
"x"], row[
"y"]))
539 flux = photoCalib.magnitudeToInstFlux(row[self.config.magVar % band], xy)
544 yield ((starIm.convertF(), xy))
547 """Remove rows from the fakes catalog which have HLR = 0 for either the buldge or disk component,
548 also remove rows that have Sersic index outside the galsim min and max allowed. (0.3 <= n <= 6.2)
552 fakeCat : `pandas.core.frame.DataFrame`
553 The catalog of fake sources to be input
557 fakeCat : `pandas.core.frame.DataFrame`
558 The input catalog of fake sources but with the bad objects removed
562 If the config option sourceSelectionColName is set then only objects with this column set to True
566 rowsToKeep = ((fakeCat[self.config.bulgeHLR] != 0.0) & (fakeCat[self.config.diskHLR] != 0.0))
567 numRowsNotUsed = len(fakeCat) - len(rowsToKeep)
568 self.log.info(
"Removing %d rows with HLR = 0 for either the bulge or disk" % numRowsNotUsed)
569 fakeCat = fakeCat[rowsToKeep]
571 minN = galsim.Sersic._minimum_n
572 maxN = galsim.Sersic._maximum_n
573 rowsToKeep = ((fakeCat[self.config.nBulge] >= minN) & (fakeCat[self.config.nBulge] <= maxN)
574 & (fakeCat[self.config.nDisk] >= minN) & (fakeCat[self.config.nDisk] <= maxN))
575 numRowsNotUsed = len(fakeCat) - len(rowsToKeep)
576 self.log.info(
"Removing %d rows with nBulge or nDisk outside of %0.2f <= n <= %0.2f" %
577 (numRowsNotUsed, minN, maxN))
579 if self.config.doSubSelectSources:
581 rowsToKeep = (fakeCat[self.config.sourceSelectionColName])
583 raise KeyError(
"Given column, %s, for source selection not found." %
584 self.config.sourceSelectionColName)
585 numRowsNotUsed = len(fakeCat) - len(rowsToKeep)
586 self.log.info(
"Removing %d rows which were not designated as template sources" % numRowsNotUsed)
588 return fakeCat[rowsToKeep]
591 """Add the fake sources to the given image
595 image : `lsst.afw.image.exposure.exposure.ExposureF`
596 The image into which the fake sources should be added
597 fakeImages : `typing.Iterator` [`tuple` ['lsst.afw.image.ImageF`, `lsst.geom.Point2d`]]
598 An iterator of tuples that contains (or generates) images of fake sources,
599 and the locations they are to be inserted at.
601 The type (star/galaxy) of fake sources input
605 image : `lsst.afw.image.exposure.exposure.ExposureF`
609 Uses the x, y information in the ``fakeCat`` to position an image of the fake interpolated onto the
610 pixel grid of the image. Sets the ``FAKE`` mask plane for the pixels added with the fake source.
613 imageBBox = image.getBBox()
614 imageMI = image.maskedImage
616 for (fakeImage, xy)
in fakeImages:
617 X0 = xy.getX() - fakeImage.getWidth()/2 + 0.5
618 Y0 = xy.getY() - fakeImage.getHeight()/2 + 0.5
619 self.log.debug(
"Adding fake source at %d, %d" % (xy.getX(), xy.getY()))
620 if sourceType ==
"galaxy":
621 interpFakeImage = afwMath.offsetImage(fakeImage, X0, Y0,
"lanczos3")
622 interpFakeImBBox = interpFakeImage.getBBox()
624 interpFakeImage = fakeImage
625 interpFakeImBBox = fakeImage.getBBox()
627 interpFakeImBBox.clip(imageBBox)
628 imageMIView = imageMI.Factory(imageMI, interpFakeImBBox)
630 if interpFakeImBBox.getArea() > 0:
631 clippedFakeImage = interpFakeImage.Factory(interpFakeImage, interpFakeImBBox)
632 clippedFakeImageMI = afwImage.MaskedImageF(clippedFakeImage)
633 clippedFakeImageMI.mask.set(self.bitmask)
634 imageMIView += clippedFakeImageMI
638 def _getMetadataName(self):
639 """Disable metadata writing"""