LSSTApplications  20.0.0
LSSTDataManagementBasePackage
insertFakes.py
Go to the documentation of this file.
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 """
23 Insert fakes into deepCoadds
24 """
25 import galsim
26 from astropy.table import Table
27 
28 import lsst.geom as geom
29 import lsst.afw.image as afwImage
30 import lsst.afw.math as afwMath
31 import lsst.pex.config as pexConfig
32 import lsst.pipe.base as pipeBase
33 
34 from lsst.pipe.base import CmdLineTask, PipelineTask, PipelineTaskConfig, PipelineTaskConnections
36 from lsst.pex.exceptions import LogicError, InvalidParameterError
37 from lsst.coadd.utils.coaddDataIdContainer import ExistingCoaddDataIdContainer
38 from lsst.geom import SpherePoint, radians, Box2D
39 from lsst.sphgeom import ConvexPolygon
40 
41 __all__ = ["InsertFakesConfig", "InsertFakesTask"]
42 
43 
44 class InsertFakesConnections(PipelineTaskConnections, defaultTemplates={"CoaddName": "deep"},
45  dimensions=("tract", "patch", "abstract_filter", "skymap")):
46 
47  image = cT.Input(
48  doc="Image into which fakes are to be added.",
49  name="{CoaddName}Coadd",
50  storageClass="ExposureF",
51  dimensions=("tract", "patch", "abstract_filter", "skymap")
52  )
53 
54  fakeCat = cT.Input(
55  doc="Catalog of fake sources to draw inputs from.",
56  name="{CoaddName}Coadd_fakeSourceCat",
57  storageClass="Parquet",
58  dimensions=("tract", "skymap")
59  )
60 
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")
66  )
67 
68 
69 class InsertFakesConfig(PipelineTaskConfig,
70  pipelineConnections=InsertFakesConnections):
71  """Config for inserting fake sources
72 
73  Notes
74  -----
75  The default column names are those from the University of Washington sims database.
76  """
77 
78  raColName = pexConfig.Field(
79  doc="RA column name used in the fake source catalog.",
80  dtype=str,
81  default="raJ2000",
82  )
83 
84  decColName = pexConfig.Field(
85  doc="Dec. column name used in the fake source catalog.",
86  dtype=str,
87  default="decJ2000",
88  )
89 
90  doCleanCat = pexConfig.Field(
91  doc="If true removes bad sources from the catalog.",
92  dtype=bool,
93  default=True,
94  )
95 
96  diskHLR = pexConfig.Field(
97  doc="Column name for the disk half light radius used in the fake source catalog.",
98  dtype=str,
99  default="DiskHalfLightRadius",
100  )
101 
102  bulgeHLR = pexConfig.Field(
103  doc="Column name for the bulge half light radius used in the fake source catalog.",
104  dtype=str,
105  default="BulgeHalfLightRadius",
106  )
107 
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.",
111  dtype=str,
112  default="%smagVar",
113  )
114 
115  nDisk = pexConfig.Field(
116  doc="The column name for the sersic index of the disk component used in the fake source catalog.",
117  dtype=str,
118  default="disk_n",
119  )
120 
121  nBulge = pexConfig.Field(
122  doc="The column name for the sersic index of the bulge component used in the fake source catalog.",
123  dtype=str,
124  default="bulge_n",
125  )
126 
127  aDisk = pexConfig.Field(
128  doc="The column name for the semi major axis length of the disk component used in the fake source"
129  "catalog.",
130  dtype=str,
131  default="a_d",
132  )
133 
134  aBulge = pexConfig.Field(
135  doc="The column name for the semi major axis length of the bulge component.",
136  dtype=str,
137  default="a_b",
138  )
139 
140  bDisk = pexConfig.Field(
141  doc="The column name for the semi minor axis length of the disk component.",
142  dtype=str,
143  default="b_d",
144  )
145 
146  bBulge = pexConfig.Field(
147  doc="The column name for the semi minor axis length of the bulge component used in the fake source "
148  "catalog.",
149  dtype=str,
150  default="b_b",
151  )
152 
153  paDisk = pexConfig.Field(
154  doc="The column name for the PA of the disk component used in the fake source catalog.",
155  dtype=str,
156  default="pa_disk",
157  )
158 
159  paBulge = pexConfig.Field(
160  doc="The column name for the PA of the bulge component used in the fake source catalog.",
161  dtype=str,
162  default="pa_bulge",
163  )
164 
165  sourceType = pexConfig.Field(
166  doc="The column name for the source type used in the fake source catalog.",
167  dtype=str,
168  default="sourceType",
169  )
170 
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"
174  "catalog.",
175  dtype=str,
176  default="static",
177  )
178 
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.",
183  dtype=float,
184  default=12.0,
185  )
186 
187  coaddName = pexConfig.Field(
188  doc="The name of the type of coadd used",
189  dtype=str,
190  default="deep",
191  )
192 
193 
194 class InsertFakesTask(PipelineTask, CmdLineTask):
195  """Insert fake objects into images.
196 
197  Add fake stars and galaxies to the given image, read in through the dataRef. Galaxy parameters are read in
198  from the specified file and then modelled using galsim.
199 
200  `InsertFakesTask` has five functions that make images of the fake sources and then add them to the
201  image.
202 
203  `addPixCoords`
204  Use the WCS information to add the pixel coordinates of each source.
205  `mkFakeGalsimGalaxies`
206  Use Galsim to make fake double sersic galaxies for each set of galaxy parameters in the input file.
207  `mkFakeStars`
208  Use the PSF information from the image to make a fake star using the magnitude information from the
209  input file.
210  `cleanCat`
211  Remove rows of the input fake catalog which have half light radius, of either the bulge or the disk,
212  that are 0.
213  `addFakeSources`
214  Add the fake sources to the image.
215 
216  """
217 
218  _DefaultName = "insertFakes"
219  ConfigClass = InsertFakesConfig
220 
221  def runDataRef(self, dataRef):
222  """Read in/write out the required data products and add fake sources to the deepCoadd.
223 
224  Parameters
225  ----------
226  dataRef : `lsst.daf.persistence.butlerSubset.ButlerDataRef`
227  Data reference defining the image to have fakes added to it
228  Used to access the following data products:
229  deepCoadd
230  """
231 
232  # To do: should it warn when asked to insert variable sources into the coadd
233 
234  if self.config.fakeType == "static":
235  fakeCat = dataRef.get("deepCoadd_fakeSourceCat").toDataFrame()
236  # To do: DM-16254, the read and write of the fake catalogs will be changed once the new pipeline
237  # task structure for ref cats is in place.
238  self.fakeSourceCatType = "deepCoadd_fakeSourceCat"
239  else:
240  fakeCat = Table.read(self.config.fakeType).to_pandas()
241 
242  coadd = dataRef.get("deepCoadd")
243  wcs = coadd.getWcs()
244  photoCalib = coadd.getPhotoCalib()
245 
246  imageWithFakes = self.run(fakeCat, coadd, wcs, photoCalib)
247 
248  dataRef.put(imageWithFakes.imageWithFakes, "fakes_deepCoadd")
249 
250  def runQuantum(self, butlerQC, inputRefs, outputRefs):
251  inputs = butlerQC.get(inputRefs)
252  inputs["wcs"] = inputs["image"].getWcs()
253  inputs["photoCalib"] = inputs["image"].getPhotoCalib()
254 
255  outputs = self.run(**inputs)
256  butlerQC.put(outputs, outputRefs)
257 
258  @classmethod
259  def _makeArgumentParser(cls):
260  parser = pipeBase.ArgumentParser(name=cls._DefaultName)
261  parser.add_id_argument(name="--id", datasetType="deepCoadd",
262  help="data IDs for the deepCoadd, e.g. --id tract=12345 patch=1,2 filter=r",
263  ContainerClass=ExistingCoaddDataIdContainer)
264  return parser
265 
266  def run(self, fakeCat, image, wcs, photoCalib):
267  """Add fake sources to an image.
268 
269  Parameters
270  ----------
271  fakeCat : `pandas.core.frame.DataFrame`
272  The catalog of fake sources to be input
273  image : `lsst.afw.image.exposure.exposure.ExposureF`
274  The image into which the fake sources should be added
275  wcs : `lsst.afw.geom.SkyWcs`
276  WCS to use to add fake sources
277  photoCalib : `lsst.afw.image.photoCalib.PhotoCalib`
278  Photometric calibration to be used to calibrate the fake sources
279 
280  Returns
281  -------
282  resultStruct : `lsst.pipe.base.struct.Struct`
283  contains : image : `lsst.afw.image.exposure.exposure.ExposureF`
284 
285  Notes
286  -----
287  Adds pixel coordinates for each source to the fakeCat and removes objects with bulge or disk half
288  light radius = 0 (if ``config.doCleanCat = True``).
289 
290  Adds the ``Fake`` mask plane to the image which is then set by `addFakeSources` to mark where fake
291  sources have been added. Uses the information in the ``fakeCat`` to make fake galaxies (using galsim)
292  and fake stars, using the PSF models from the PSF information for the image. These are then added to
293  the image and the image with fakes included returned.
294 
295  The galsim galaxies are made using a double sersic profile, one for the bulge and one for the disk,
296  this is then convolved with the PSF at that point.
297  """
298 
299  image.mask.addMaskPlane("FAKE")
300  self.bitmask = image.mask.getPlaneBitMask("FAKE")
301  self.log.info("Adding mask plane with bitmask %d" % self.bitmask)
302 
303  fakeCat = self.addPixCoords(fakeCat, wcs)
304  if self.config.doCleanCat:
305  fakeCat = self.cleanCat(fakeCat)
306  fakeCat = self.trimFakeCat(fakeCat, image, wcs)
307 
308  band = image.getFilter().getName()
309  pixelScale = wcs.getPixelScale().asArcseconds()
310  psf = image.getPsf()
311 
312  galaxies = (fakeCat[self.config.sourceType] == "galaxy")
313  galImages = self.mkFakeGalsimGalaxies(fakeCat[galaxies], band, photoCalib, pixelScale, psf, image)
314  image = self.addFakeSources(image, galImages, "galaxy")
315 
316  stars = (fakeCat[self.config.sourceType] == "star")
317  starImages = self.mkFakeStars(fakeCat[stars], band, photoCalib, psf, image)
318  image = self.addFakeSources(image, starImages, "star")
319  resultStruct = pipeBase.Struct(imageWithFakes=image)
320 
321  return resultStruct
322 
323  def addPixCoords(self, fakeCat, wcs):
324 
325  """Add pixel coordinates to the catalog of fakes.
326 
327  Parameters
328  ----------
329  fakeCat : `pandas.core.frame.DataFrame`
330  The catalog of fake sources to be input
331  wcs : `lsst.afw.geom.SkyWcs`
332  WCS to use to add fake sources
333 
334  Returns
335  -------
336  fakeCat : `pandas.core.frame.DataFrame`
337 
338  Notes
339  -----
340  The default option is to use the WCS information from the image. If the ``useUpdatedCalibs`` config
341  option is set then it will use the updated WCS from jointCal.
342  """
343 
344  ras = fakeCat[self.config.raColName].values
345  decs = fakeCat[self.config.decColName].values
346  skyCoords = [SpherePoint(ra, dec, radians) for (ra, dec) in zip(ras, decs)]
347  pixCoords = wcs.skyToPixel(skyCoords)
348  xs = [coord.getX() for coord in pixCoords]
349  ys = [coord.getY() for coord in pixCoords]
350  fakeCat["x"] = xs
351  fakeCat["y"] = ys
352 
353  return fakeCat
354 
355  def trimFakeCat(self, fakeCat, image, wcs):
356  """Trim the fake cat to about the size of the input image.
357 
358  Parameters
359  ----------
360  fakeCat : `pandas.core.frame.DataFrame`
361  The catalog of fake sources to be input
362  image : `lsst.afw.image.exposure.exposure.ExposureF`
363  The image into which the fake sources should be added
364  wcs : `lsst.afw.geom.SkyWcs`
365  WCS to use to add fake sources
366 
367  Returns
368  -------
369  fakeCat : `pandas.core.frame.DataFrame`
370  The original fakeCat trimmed to the area of the image
371  """
372 
373  bbox = Box2D(image.getBBox())
374  corners = bbox.getCorners()
375 
376  skyCorners = wcs.pixelToSky(corners)
377  region = ConvexPolygon([s.getVector() for s in skyCorners])
378 
379  def trim(row):
380  coord = SpherePoint(row[self.config.raColName], row[self.config.decColName], radians)
381  return region.contains(coord.getVector())
382 
383  return fakeCat[fakeCat.apply(trim, axis=1)]
384 
385  def mkFakeGalsimGalaxies(self, fakeCat, band, photoCalib, pixelScale, psf, image):
386  """Make images of fake galaxies using GalSim.
387 
388  Parameters
389  ----------
390  band : `str`
391  pixelScale : `float`
392  psf : `lsst.meas.extensions.psfex.psfexPsf.PsfexPsf`
393  The PSF information to use to make the PSF images
394  fakeCat : `pandas.core.frame.DataFrame`
395  The catalog of fake sources to be input
396  photoCalib : `lsst.afw.image.photoCalib.PhotoCalib`
397  Photometric calibration to be used to calibrate the fake sources
398 
399  Yields
400  -------
401  galImages : `generator`
402  A generator of tuples of `lsst.afw.image.exposure.exposure.ExposureF` and
403  `lsst.geom.Point2D` of their locations.
404 
405  Notes
406  -----
407 
408  Fake galaxies are made by combining two sersic profiles, one for the bulge and one for the disk. Each
409  component has an individual sersic index (n), a, b and position angle (PA). The combined profile is
410  then convolved with the PSF at the specified x, y position on the image.
411 
412  The names of the columns in the ``fakeCat`` are configurable and are the column names from the
413  University of Washington simulations database as default. For more information see the doc strings
414  attached to the config options.
415 
416  See mkFakeStars doc string for an explanation of calibration to instrumental flux.
417  """
418 
419  self.log.info("Making %d fake galaxy images" % len(fakeCat))
420 
421  for (index, row) in fakeCat.iterrows():
422  xy = geom.Point2D(row["x"], row["y"])
423 
424  try:
425  correctedFlux = psf.computeApertureFlux(self.config.calibFluxRadius, xy)
426  psfKernel = psf.computeKernelImage(xy).getArray()
427  psfKernel /= correctedFlux
428 
429  except InvalidParameterError:
430  self.log.info("Galaxy at %0.4f, %0.4f outside of image" % (row["x"], row["y"]))
431  continue
432 
433  try:
434  flux = photoCalib.magnitudeToInstFlux(row[self.config.magVar % band], xy)
435  except LogicError:
436  flux = 0
437 
438  bulge = galsim.Sersic(row[self.config.nBulge], half_light_radius=row[self.config.bulgeHLR])
439  axisRatioBulge = row[self.config.bBulge]/row[self.config.aBulge]
440  bulge = bulge.shear(q=axisRatioBulge, beta=((90 - row[self.config.paBulge])*galsim.degrees))
441 
442  disk = galsim.Sersic(row[self.config.nDisk], half_light_radius=row[self.config.diskHLR])
443  axisRatioDisk = row[self.config.bDisk]/row[self.config.aDisk]
444  disk = disk.shear(q=axisRatioDisk, beta=((90 - row[self.config.paDisk])*galsim.degrees))
445 
446  gal = disk + bulge
447  gal = gal.withFlux(flux)
448 
449  psfIm = galsim.InterpolatedImage(galsim.Image(psfKernel), scale=pixelScale)
450  gal = galsim.Convolve([gal, psfIm])
451  try:
452  galIm = gal.drawImage(scale=pixelScale, method="real_space").array
453  except (galsim.errors.GalSimFFTSizeError, MemoryError):
454  continue
455 
456  yield (afwImage.ImageF(galIm), xy)
457 
458  def mkFakeStars(self, fakeCat, band, photoCalib, psf, image):
459 
460  """Make fake stars based off the properties in the fakeCat.
461 
462  Parameters
463  ----------
464  band : `str`
465  psf : `lsst.meas.extensions.psfex.psfexPsf.PsfexPsf`
466  The PSF information to use to make the PSF images
467  fakeCat : `pandas.core.frame.DataFrame`
468  The catalog of fake sources to be input
469  image : `lsst.afw.image.exposure.exposure.ExposureF`
470  The image into which the fake sources should be added
471  photoCalib : `lsst.afw.image.photoCalib.PhotoCalib`
472  Photometric calibration to be used to calibrate the fake sources
473 
474  Yields
475  -------
476  starImages : `generator`
477  A generator of tuples of `lsst.afw.image.ImageF` of fake stars and
478  `lsst.geom.Point2D` of their locations.
479 
480  Notes
481  -----
482  To take a given magnitude and translate to the number of counts in the image
483  we use photoCalib.magnitudeToInstFlux, which returns the instrumental flux for the
484  given calibration radius used in the photometric calibration step.
485  Thus `calibFluxRadius` should be set to this same radius so that we can normalize
486  the PSF model to the correct instrumental flux within calibFluxRadius.
487  """
488 
489  self.log.info("Making %d fake star images" % len(fakeCat))
490 
491  for (index, row) in fakeCat.iterrows():
492  xy = geom.Point2D(row["x"], row["y"])
493 
494  # We put these two PSF calculations within this same try block so that we catch cases
495  # where the object's position is outside of the image.
496  try:
497  correctedFlux = psf.computeApertureFlux(self.config.calibFluxRadius, xy)
498  starIm = psf.computeImage(xy)
499  starIm /= correctedFlux
500 
501  except InvalidParameterError:
502  self.log.info("Star at %0.4f, %0.4f outside of image" % (row["x"], row["y"]))
503  continue
504 
505  try:
506  flux = photoCalib.magnitudeToInstFlux(row[self.config.magVar % band], xy)
507  except LogicError:
508  flux = 0
509 
510  starIm *= flux
511  yield ((starIm.convertF(), xy))
512 
513  def cleanCat(self, fakeCat):
514  """Remove rows from the fakes catalog which have HLR = 0 for either the buldge or disk component
515 
516  Parameters
517  ----------
518  fakeCat : `pandas.core.frame.DataFrame`
519  The catalog of fake sources to be input
520 
521  Returns
522  -------
523  fakeCat : `pandas.core.frame.DataFrame`
524  The input catalog of fake sources but with the bad objects removed
525  """
526 
527  goodRows = ((fakeCat[self.config.bulgeHLR] != 0.0) & (fakeCat[self.config.diskHLR] != 0.0))
528 
529  badRows = len(fakeCat) - len(goodRows)
530  self.log.info("Removing %d rows with HLR = 0 for either the bulge or disk" % badRows)
531 
532  return fakeCat[goodRows]
533 
534  def addFakeSources(self, image, fakeImages, sourceType):
535  """Add the fake sources to the given image
536 
537  Parameters
538  ----------
539  image : `lsst.afw.image.exposure.exposure.ExposureF`
540  The image into which the fake sources should be added
541  fakeImages : `typing.Iterator` [`tuple` ['lsst.afw.image.ImageF`, `lsst.geom.Point2d`]]
542  An iterator of tuples that contains (or generates) images of fake sources,
543  and the locations they are to be inserted at.
544  sourceType : `str`
545  The type (star/galaxy) of fake sources input
546 
547  Returns
548  -------
549  image : `lsst.afw.image.exposure.exposure.ExposureF`
550 
551  Notes
552  -----
553  Uses the x, y information in the ``fakeCat`` to position an image of the fake interpolated onto the
554  pixel grid of the image. Sets the ``FAKE`` mask plane for the pixels added with the fake source.
555  """
556 
557  imageBBox = image.getBBox()
558  imageMI = image.maskedImage
559 
560  for (fakeImage, xy) in fakeImages:
561  X0 = xy.getX() - fakeImage.getWidth()/2 + 0.5
562  Y0 = xy.getY() - fakeImage.getHeight()/2 + 0.5
563  self.log.debug("Adding fake source at %d, %d" % (xy.getX(), xy.getY()))
564  if sourceType == "galaxy":
565  interpFakeImage = afwMath.offsetImage(fakeImage, X0, Y0, "lanczos3")
566  interpFakeImBBox = interpFakeImage.getBBox()
567  else:
568  interpFakeImage = fakeImage
569  interpFakeImBBox = fakeImage.getBBox()
570 
571  interpFakeImBBox.clip(imageBBox)
572  imageMIView = imageMI.Factory(imageMI, interpFakeImBBox)
573 
574  if interpFakeImBBox.getArea() > 0:
575  clippedFakeImage = interpFakeImage.Factory(interpFakeImage, interpFakeImBBox)
576  clippedFakeImageMI = afwImage.MaskedImageF(clippedFakeImage)
577  clippedFakeImageMI.mask.set(self.bitmask)
578  imageMIView += clippedFakeImageMI
579 
580  return image
581 
582  def _getMetadataName(self):
583  """Disable metadata writing"""
584  return None
lsst.pipe.base.connections.PipelineTaskConnections.config
config
Definition: connections.py:368
lsst::afw::image
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects.
Definition: imageAlgorithm.dox:1
lsst::log.log.logContinued.info
def info(fmt, *args)
Definition: logContinued.py:198
lsst.gdb.afw.printers.debug
bool debug
Definition: printers.py:9
lsst.pipe.tasks.insertFakes.InsertFakesConnections
Definition: insertFakes.py:44
lsst::sphgeom::ConvexPolygon
ConvexPolygon is a closed convex polygon on the unit sphere.
Definition: ConvexPolygon.h:57
lsst::sphgeom
Definition: Angle.h:38
lsst.pipe.tasks.assembleCoadd.run
def run(self, skyInfo, tempExpRefList, imageScalerList, weightList, altMaskList=None, mask=None, supplementaryData=None)
Definition: assembleCoadd.py:712
lsst.pipe.base.pipelineTask.PipelineTask
Definition: pipelineTask.py:32
lsst.pipe.base.connections.PipelineTaskConnections
Definition: connections.py:254
lsst.pipe.base.config.PipelineTaskConfig
Definition: config.py:113
lsst::geom
Definition: geomOperators.dox:4
lsst::afw::math
Definition: statistics.dox:6
lsst::geom::Point< double, 2 >
lsst::afw::math::offsetImage
std::shared_ptr< ImageT > offsetImage(ImageT const &image, float dx, float dy, std::string const &algorithmName="lanczos5", unsigned int buffer=0)
Return an image offset by (dx, dy) using the specified algorithm.
Definition: offsetImage.cc:41
lsst::pex::exceptions
Definition: Exception.h:37
lsst::geom::SpherePoint
Point in an unspecified spherical coordinate system.
Definition: SpherePoint.h:57
lsst::coadd::utils.coaddDataIdContainer
Definition: coaddDataIdContainer.py:1
lsst::geom::Box2D
A floating-point coordinate rectangle geometry.
Definition: Box.h:413
lsst.pipe.base
Definition: __init__.py:1
lsst.pipe.base.connectionTypes
Definition: connectionTypes.py:1
lsst.pipe.base.cmdLineTask.CmdLineTask
Definition: cmdLineTask.py:492