lsst.pipe.tasks  21.0.0-80-g62ad60b1+450980b591
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 import numpy as np
28 
29 import lsst.geom as geom
30 import lsst.afw.image as afwImage
31 import lsst.afw.math as afwMath
32 import lsst.pex.config as pexConfig
33 import lsst.pipe.base as pipeBase
34 
35 from lsst.pipe.base import CmdLineTask, PipelineTask, PipelineTaskConfig, PipelineTaskConnections
37 from lsst.pex.exceptions import LogicError, InvalidParameterError
38 from lsst.coadd.utils.coaddDataIdContainer import ExistingCoaddDataIdContainer
39 from lsst.geom import SpherePoint, radians, Box2D
40 
41 __all__ = ["InsertFakesConfig", "InsertFakesTask"]
42 
43 
44 def _add_fake_sources(exposure, objects, calibFluxRadius=12.0, logger=None):
45  """Add fake sources to the given exposure
46 
47  Parameters
48  ----------
49  exposure : `lsst.afw.image.exposure.exposure.ExposureF`
50  The exposure into which the fake sources should be added
51  objects : `typing.Iterator` [`tuple` ['lsst.geom.SpherePoint`, `galsim.GSObject`]]
52  An iterator of tuples that contains (or generates) locations and object
53  surface brightness profiles to inject.
54  calibFluxRadius : `float`, optional
55  Aperture radius (in pixels) used to define the calibration for this
56  exposure+catalog. This is used to produce the correct instrumental fluxes
57  within the radius. The value should match that of the field defined in
58  slot_CalibFlux_instFlux.
59  logger : `lsst.log.log.log.Log` or `logging.Logger`, optional
60  Logger.
61  """
62  exposure.mask.addMaskPlane("FAKE")
63  bitmask = exposure.mask.getPlaneBitMask("FAKE")
64  if logger:
65  logger.info(f"Adding mask plane with bitmask {bitmask}")
66 
67  wcs = exposure.getWcs()
68  psf = exposure.getPsf()
69 
70  bbox = exposure.getBBox()
71  fullBounds = galsim.BoundsI(bbox.minX, bbox.maxX, bbox.minY, bbox.maxY)
72  gsImg = galsim.Image(exposure.image.array, bounds=fullBounds)
73 
74  for spt, gsObj in objects:
75  pt = wcs.skyToPixel(spt)
76  posd = galsim.PositionD(pt.x, pt.y)
77  posi = galsim.PositionI(pt.x//1, pt.y//1)
78  if logger:
79  logger.debug(f"Adding fake source at {pt}")
80 
81  mat = wcs.linearizePixelToSky(spt, geom.arcseconds).getMatrix()
82  gsWCS = galsim.JacobianWCS(mat[0, 0], mat[0, 1], mat[1, 0], mat[1, 1])
83 
84  psfArr = psf.computeKernelImage(pt).array
85  apCorr = psf.computeApertureFlux(calibFluxRadius)
86  psfArr /= apCorr
87  gsPSF = galsim.InterpolatedImage(galsim.Image(psfArr), wcs=gsWCS)
88 
89  conv = galsim.Convolve(gsObj, gsPSF)
90  stampSize = conv.getGoodImageSize(gsWCS.minLinearScale())
91  subBounds = galsim.BoundsI(posi).withBorder(stampSize//2)
92  subBounds &= fullBounds
93 
94  if subBounds.area() > 0:
95  subImg = gsImg[subBounds]
96  offset = posd - subBounds.true_center
97  # Note, for calexp injection, pixel is already part of the PSF and
98  # for coadd injection, it's incorrect to include the output pixel.
99  # So for both cases, we draw using method='no_pixel'.
100  conv.drawImage(
101  subImg,
102  add_to_image=True,
103  offset=offset,
104  wcs=gsWCS,
105  method='no_pixel'
106  )
107 
108  subBox = geom.Box2I(
109  geom.Point2I(subBounds.xmin, subBounds.ymin),
110  geom.Point2I(subBounds.xmax, subBounds.ymax)
111  )
112  exposure[subBox].mask.array |= bitmask
113 
114 
115 def _isWCSGalsimDefault(wcs, hdr):
116  """Decide if wcs = galsim.PixelScale(1.0) is explicitly present in header,
117  or if it's just the galsim default.
118 
119  Parameters
120  ----------
121  wcs : galsim.BaseWCS
122  Potentially default WCS.
123  hdr : galsim.fits.FitsHeader
124  Header as read in by galsim.
125 
126  Returns
127  -------
128  isDefault : bool
129  True if default, False if explicitly set in header.
130  """
131  if wcs != galsim.PixelScale(1.0):
132  return False
133  if hdr.get('GS_WCS') is not None:
134  return False
135  if hdr.get('CTYPE1', 'LINEAR') == 'LINEAR':
136  return not any(k in hdr for k in ['CD1_1', 'CDELT1'])
137  for wcs_type in galsim.fitswcs.fits_wcs_types:
138  # If one of these succeeds, then assume result is explicit
139  try:
140  wcs_type._readHeader(hdr)
141  return False
142  except Exception:
143  pass
144  else:
145  return not any(k in hdr for k in ['CD1_1', 'CDELT1'])
146 
147 
148 class InsertFakesConnections(PipelineTaskConnections,
149  defaultTemplates={"coaddName": "deep",
150  "fakesType": "fakes_"},
151  dimensions=("tract", "patch", "band", "skymap")):
152 
153  image = cT.Input(
154  doc="Image into which fakes are to be added.",
155  name="{coaddName}Coadd",
156  storageClass="ExposureF",
157  dimensions=("tract", "patch", "band", "skymap")
158  )
159 
160  fakeCat = cT.Input(
161  doc="Catalog of fake sources to draw inputs from.",
162  name="{fakesType}fakeSourceCat",
163  storageClass="DataFrame",
164  dimensions=("tract", "skymap")
165  )
166 
167  imageWithFakes = cT.Output(
168  doc="Image with fake sources added.",
169  name="{fakesType}{coaddName}Coadd",
170  storageClass="ExposureF",
171  dimensions=("tract", "patch", "band", "skymap")
172  )
173 
174 
175 class InsertFakesConfig(PipelineTaskConfig,
176  pipelineConnections=InsertFakesConnections):
177  """Config for inserting fake sources
178 
179  Notes
180  -----
181  The default column names are those from the University of Washington sims database.
182  """
183 
184  raColName = pexConfig.Field(
185  doc="RA column name used in the fake source catalog.",
186  dtype=str,
187  default="raJ2000",
188  )
189 
190  decColName = pexConfig.Field(
191  doc="Dec. column name used in the fake source catalog.",
192  dtype=str,
193  default="decJ2000",
194  )
195 
196  doCleanCat = pexConfig.Field(
197  doc="If true removes bad sources from the catalog.",
198  dtype=bool,
199  default=True,
200  )
201 
202  diskHLR = pexConfig.Field(
203  doc="Column name for the disk half light radius used in the fake source catalog.",
204  dtype=str,
205  default="DiskHalfLightRadius",
206  )
207 
208  bulgeHLR = pexConfig.Field(
209  doc="Column name for the bulge half light radius used in the fake source catalog.",
210  dtype=str,
211  default="BulgeHalfLightRadius",
212  )
213 
214  magVar = pexConfig.Field(
215  doc="The column name for the magnitude calculated taking variability into account. In the format "
216  "``filter name``magVar, e.g. imagVar for the magnitude in the i band.",
217  dtype=str,
218  default="%smagVar",
219  )
220 
221  nDisk = pexConfig.Field(
222  doc="The column name for the sersic index of the disk component used in the fake source catalog.",
223  dtype=str,
224  default="disk_n",
225  )
226 
227  nBulge = pexConfig.Field(
228  doc="The column name for the sersic index of the bulge component used in the fake source catalog.",
229  dtype=str,
230  default="bulge_n",
231  )
232 
233  aDisk = pexConfig.Field(
234  doc="The column name for the semi major axis length of the disk component used in the fake source"
235  "catalog.",
236  dtype=str,
237  default="a_d",
238  )
239 
240  aBulge = pexConfig.Field(
241  doc="The column name for the semi major axis length of the bulge component.",
242  dtype=str,
243  default="a_b",
244  )
245 
246  bDisk = pexConfig.Field(
247  doc="The column name for the semi minor axis length of the disk component.",
248  dtype=str,
249  default="b_d",
250  )
251 
252  bBulge = pexConfig.Field(
253  doc="The column name for the semi minor axis length of the bulge component used in the fake source "
254  "catalog.",
255  dtype=str,
256  default="b_b",
257  )
258 
259  paDisk = pexConfig.Field(
260  doc="The column name for the PA of the disk component used in the fake source catalog.",
261  dtype=str,
262  default="pa_disk",
263  )
264 
265  paBulge = pexConfig.Field(
266  doc="The column name for the PA of the bulge component used in the fake source catalog.",
267  dtype=str,
268  default="pa_bulge",
269  )
270 
271  sourceType = pexConfig.Field(
272  doc="The column name for the source type used in the fake source catalog.",
273  dtype=str,
274  default="sourceType",
275  )
276 
277  fakeType = pexConfig.Field(
278  doc="What type of fake catalog to use, snapshot (includes variability in the magnitudes calculated "
279  "from the MJD of the image), static (no variability) or filename for a user defined fits"
280  "catalog.",
281  dtype=str,
282  default="static",
283  )
284 
285  calibFluxRadius = pexConfig.Field(
286  doc="Aperture radius (in pixels) that was used to define the calibration for this image+catalog. "
287  "This will be used to produce the correct instrumental fluxes within the radius. "
288  "This value should match that of the field defined in slot_CalibFlux_instFlux.",
289  dtype=float,
290  default=12.0,
291  )
292 
293  coaddName = pexConfig.Field(
294  doc="The name of the type of coadd used",
295  dtype=str,
296  default="deep",
297  )
298 
299  doSubSelectSources = pexConfig.Field(
300  doc="Set to True if you wish to sub select sources to be input based on the value in the column"
301  "set in the sourceSelectionColName config option.",
302  dtype=bool,
303  default=False
304  )
305 
306  sourceSelectionColName = pexConfig.Field(
307  doc="The name of the column in the input fakes catalogue to be used to determine which sources to"
308  "add, default is none and when this is used all sources are added.",
309  dtype=str,
310  default="templateSource"
311  )
312 
313  insertImages = pexConfig.Field(
314  doc="Insert images directly? True or False.",
315  dtype=bool,
316  default=False,
317  )
318 
319  doProcessAllDataIds = pexConfig.Field(
320  doc="If True, all input data IDs will be processed, even those containing no fake sources.",
321  dtype=bool,
322  default=False,
323  )
324 
325  trimBuffer = pexConfig.Field(
326  doc="Size of the pixel buffer surrounding the image. Only those fake sources with a centroid"
327  "falling within the image+buffer region will be considered for fake source injection.",
328  dtype=int,
329  default=100,
330  )
331 
332 
333 class InsertFakesTask(PipelineTask, CmdLineTask):
334  """Insert fake objects into images.
335 
336  Add fake stars and galaxies to the given image, read in through the dataRef. Galaxy parameters are read in
337  from the specified file and then modelled using galsim.
338 
339  `InsertFakesTask` has five functions that make images of the fake sources and then add them to the
340  image.
341 
342  `addPixCoords`
343  Use the WCS information to add the pixel coordinates of each source.
344  `mkFakeGalsimGalaxies`
345  Use Galsim to make fake double sersic galaxies for each set of galaxy parameters in the input file.
346  `mkFakeStars`
347  Use the PSF information from the image to make a fake star using the magnitude information from the
348  input file.
349  `cleanCat`
350  Remove rows of the input fake catalog which have half light radius, of either the bulge or the disk,
351  that are 0. Also removes rows that have Sersic index outside of galsim's allowed paramters. If
352  the config option sourceSelectionColName is set then this function limits the catalog of input fakes
353  to only those which are True in this column.
354  `addFakeSources`
355  Add the fake sources to the image.
356 
357  """
358 
359  _DefaultName = "insertFakes"
360  ConfigClass = InsertFakesConfig
361 
362  def runDataRef(self, dataRef):
363  """Read in/write out the required data products and add fake sources to the deepCoadd.
364 
365  Parameters
366  ----------
367  dataRef : `lsst.daf.persistence.butlerSubset.ButlerDataRef`
368  Data reference defining the image to have fakes added to it
369  Used to access the following data products:
370  deepCoadd
371  """
372 
373  infoStr = "Adding fakes to: tract: %d, patch: %s, filter: %s" % (dataRef.dataId["tract"],
374  dataRef.dataId["patch"],
375  dataRef.dataId["filter"])
376  self.log.info(infoStr)
377 
378  # To do: should it warn when asked to insert variable sources into the coadd
379 
380  if self.config.fakeType == "static":
381  fakeCat = dataRef.get("deepCoadd_fakeSourceCat").toDataFrame()
382  # To do: DM-16254, the read and write of the fake catalogs will be changed once the new pipeline
383  # task structure for ref cats is in place.
384  self.fakeSourceCatType = "deepCoadd_fakeSourceCat"
385  else:
386  fakeCat = Table.read(self.config.fakeType).to_pandas()
387 
388  coadd = dataRef.get("deepCoadd")
389  wcs = coadd.getWcs()
390  photoCalib = coadd.getPhotoCalib()
391 
392  imageWithFakes = self.run(fakeCat, coadd, wcs, photoCalib)
393 
394  dataRef.put(imageWithFakes.imageWithFakes, "fakes_deepCoadd")
395 
396  def runQuantum(self, butlerQC, inputRefs, outputRefs):
397  inputs = butlerQC.get(inputRefs)
398  inputs["wcs"] = inputs["image"].getWcs()
399  inputs["photoCalib"] = inputs["image"].getPhotoCalib()
400 
401  outputs = self.run(**inputs)
402  butlerQC.put(outputs, outputRefs)
403 
404  @classmethod
405  def _makeArgumentParser(cls):
406  parser = pipeBase.ArgumentParser(name=cls._DefaultName)
407  parser.add_id_argument(name="--id", datasetType="deepCoadd",
408  help="data IDs for the deepCoadd, e.g. --id tract=12345 patch=1,2 filter=r",
409  ContainerClass=ExistingCoaddDataIdContainer)
410  return parser
411 
412  def run(self, fakeCat, image, wcs, photoCalib):
413  """Add fake sources to an image.
414 
415  Parameters
416  ----------
417  fakeCat : `pandas.core.frame.DataFrame`
418  The catalog of fake sources to be input
419  image : `lsst.afw.image.exposure.exposure.ExposureF`
420  The image into which the fake sources should be added
421  wcs : `lsst.afw.geom.SkyWcs`
422  WCS to use to add fake sources
423  photoCalib : `lsst.afw.image.photoCalib.PhotoCalib`
424  Photometric calibration to be used to calibrate the fake sources
425 
426  Returns
427  -------
428  resultStruct : `lsst.pipe.base.struct.Struct`
429  contains : image : `lsst.afw.image.exposure.exposure.ExposureF`
430 
431  Notes
432  -----
433  Adds pixel coordinates for each source to the fakeCat and removes objects with bulge or disk half
434  light radius = 0 (if ``config.doCleanCat = True``).
435 
436  Adds the ``Fake`` mask plane to the image which is then set by `addFakeSources` to mark where fake
437  sources have been added. Uses the information in the ``fakeCat`` to make fake galaxies (using galsim)
438  and fake stars, using the PSF models from the PSF information for the image. These are then added to
439  the image and the image with fakes included returned.
440 
441  The galsim galaxies are made using a double sersic profile, one for the bulge and one for the disk,
442  this is then convolved with the PSF at that point.
443  """
444  # Attach overriding wcs and photoCalib to image, but retain originals
445  # so we can reset at the end.
446  origWcs = image.getWcs()
447  origPhotoCalib = image.getPhotoCalib()
448  image.setWcs(wcs)
449  image.setPhotoCalib(photoCalib)
450 
451  fakeCat = self.addPixCoords(fakeCat, image)
452  fakeCat = self.trimFakeCat(fakeCat, image)
453 
454  if len(fakeCat) > 0:
455  if isinstance(fakeCat[self.config.sourceType].iloc[0], str):
456  galCheckVal = "galaxy"
457  starCheckVal = "star"
458  elif isinstance(fakeCat[self.config.sourceType].iloc[0], bytes):
459  galCheckVal = b"galaxy"
460  starCheckVal = b"star"
461  elif isinstance(fakeCat[self.config.sourceType].iloc[0], (int, float)):
462  galCheckVal = 1
463  starCheckVal = 0
464  else:
465  raise TypeError("sourceType column does not have required type, should be str, bytes or int")
466 
467  if not self.config.insertImages:
468  if self.config.doCleanCat:
469  fakeCat = self.cleanCat(fakeCat, starCheckVal)
470 
471  generator = self._generateGSObjectsFromCatalog(image, fakeCat, galCheckVal, starCheckVal)
472  else:
473  generator = self._generateGSObjectsFromImages(image, fakeCat)
474  _add_fake_sources(image, generator, calibFluxRadius=self.config.calibFluxRadius, logger=self.log)
475  elif len(fakeCat) == 0 and self.config.doProcessAllDataIds:
476  self.log.warn("No fakes found for this dataRef; processing anyway.")
477  else:
478  raise RuntimeError("No fakes found for this dataRef.")
479 
480  # restore original exposure WCS and photoCalib
481  image.setWcs(origWcs)
482  image.setPhotoCalib(origPhotoCalib)
483 
484  resultStruct = pipeBase.Struct(imageWithFakes=image)
485 
486  return resultStruct
487 
488  def _generateGSObjectsFromCatalog(self, exposure, fakeCat, galCheckVal, starCheckVal):
489  """Process catalog to generate `galsim.GSObject` s.
490 
491  Parameters
492  ----------
493  exposure : `lsst.afw.image.exposure.exposure.ExposureF`
494  The exposure into which the fake sources should be added
495  fakeCat : `pandas.core.frame.DataFrame`
496  The catalog of fake sources to be input
497  galCheckVal : `str`, `bytes` or `int`
498  The value that is set in the sourceType column to specifiy an object is a galaxy.
499  starCheckVal : `str`, `bytes` or `int`
500  The value that is set in the sourceType column to specifiy an object is a star.
501 
502  Yields
503  ------
504  gsObjects : `generator`
505  A generator of tuples of `lsst.geom.SpherePoint` and `galsim.GSObject`.
506  """
507  band = exposure.getFilterLabel().bandLabel
508  wcs = exposure.getWcs()
509  photoCalib = exposure.getPhotoCalib()
510 
511  self.log.info(f"Making {len(fakeCat)} objects for insertion")
512 
513  for (index, row) in fakeCat.iterrows():
514  ra = row[self.config.raColName]
515  dec = row[self.config.decColName]
516  skyCoord = SpherePoint(ra, dec, radians)
517  xy = wcs.skyToPixel(skyCoord)
518 
519  try:
520  flux = photoCalib.magnitudeToInstFlux(row[self.config.magVar % band], xy)
521  except LogicError:
522  continue
523 
524  sourceType = row[self.config.sourceType]
525  if sourceType == galCheckVal:
526  bulge = galsim.Sersic(n=row[self.config.nBulge], half_light_radius=row[self.config.bulgeHLR])
527  axisRatioBulge = row[self.config.bBulge]/row[self.config.aBulge]
528  bulge = bulge.shear(q=axisRatioBulge, beta=((90 - row[self.config.paBulge])*galsim.degrees))
529 
530  disk = galsim.Sersic(n=row[self.config.nDisk], half_light_radius=row[self.config.diskHLR])
531  axisRatioDisk = row[self.config.bDisk]/row[self.config.aDisk]
532  disk = disk.shear(q=axisRatioDisk, beta=((90 - row[self.config.paDisk])*galsim.degrees))
533 
534  gal = bulge + disk
535  gal = gal.withFlux(flux)
536 
537  yield skyCoord, gal
538  elif sourceType == starCheckVal:
539  star = galsim.DeltaFunction()
540  star = star.withFlux(flux)
541  yield skyCoord, star
542  else:
543  raise TypeError(f"Unknown sourceType {sourceType}")
544 
545  def _generateGSObjectsFromImages(self, exposure, fakeCat):
546  """Process catalog to generate `galsim.GSObject` s.
547 
548  Parameters
549  ----------
550  exposure : `lsst.afw.image.exposure.exposure.ExposureF`
551  The exposure into which the fake sources should be added
552  fakeCat : `pandas.core.frame.DataFrame`
553  The catalog of fake sources to be input
554 
555  Yields
556  ------
557  gsObjects : `generator`
558  A generator of tuples of `lsst.geom.SpherePoint` and `galsim.GSObject`.
559  """
560  band = exposure.getFilterLabel().bandLabel
561  wcs = exposure.getWcs()
562  photoCalib = exposure.getPhotoCalib()
563 
564  self.log.info(f"Processing {len(fakeCat)} fake images")
565 
566  for (index, row) in fakeCat.iterrows():
567  ra = row[self.config.raColName]
568  dec = row[self.config.decColName]
569  skyCoord = SpherePoint(ra, dec, radians)
570  xy = wcs.skyToPixel(skyCoord)
571 
572  try:
573  flux = photoCalib.magnitudeToInstFlux(row[self.config.magVar % band], xy)
574  except LogicError:
575  continue
576 
577  imFile = row[band+"imFilename"]
578  try:
579  imFile = imFile.decode("utf-8")
580  except AttributeError:
581  pass
582  imFile = imFile.strip()
583  im = galsim.fits.read(imFile, read_header=True)
584 
585  # GalSim will always attach a WCS to the image read in as above. If
586  # it can't find a WCS in the header, then it defaults to scale = 1.0
587  # arcsec / pix. So if that's the scale, then we need to check if it
588  # was explicitly set or if it's just the default. If it's just the
589  # default then we should override with the pixel scale of the target
590  # image.
591  if _isWCSGalsimDefault(im.wcs, im.header):
592  im.wcs = galsim.PixelScale(
593  wcs.getPixelScale().asArcseconds()
594  )
595 
596  obj = galsim.InterpolatedImage(im)
597  obj = obj.withFlux(flux)
598  yield skyCoord, obj
599 
600  def processImagesForInsertion(self, fakeCat, wcs, psf, photoCalib, band, pixelScale):
601  """Process images from files into the format needed for insertion.
602 
603  Parameters
604  ----------
605  fakeCat : `pandas.core.frame.DataFrame`
606  The catalog of fake sources to be input
607  wcs : `lsst.afw.geom.skyWcs.skyWcs.SkyWc`
608  WCS to use to add fake sources
609  psf : `lsst.meas.algorithms.coaddPsf.coaddPsf.CoaddPsf` or
610  `lsst.meas.extensions.psfex.psfexPsf.PsfexPsf`
611  The PSF information to use to make the PSF images
612  photoCalib : `lsst.afw.image.photoCalib.PhotoCalib`
613  Photometric calibration to be used to calibrate the fake sources
614  band : `str`
615  The filter band that the observation was taken in.
616  pixelScale : `float`
617  The pixel scale of the image the sources are to be added to.
618 
619  Returns
620  -------
621  galImages : `list`
622  A list of tuples of `lsst.afw.image.exposure.exposure.ExposureF` and
623  `lsst.geom.Point2D` of their locations.
624  For sources labelled as galaxy.
625  starImages : `list`
626  A list of tuples of `lsst.afw.image.exposure.exposure.ExposureF` and
627  `lsst.geom.Point2D` of their locations.
628  For sources labelled as star.
629 
630  Notes
631  -----
632  The input fakes catalog needs to contain the absolute path to the image in the
633  band that is being used to add images to. It also needs to have the R.A. and
634  declination of the fake source in radians and the sourceType of the object.
635  """
636  galImages = []
637  starImages = []
638 
639  self.log.info("Processing %d fake images" % len(fakeCat))
640 
641  for (imFile, sourceType, mag, x, y) in zip(fakeCat[band + "imFilename"].array,
642  fakeCat["sourceType"].array,
643  fakeCat[self.config.magVar % band].array,
644  fakeCat["x"].array, fakeCat["y"].array):
645 
646  im = afwImage.ImageF.readFits(imFile)
647 
648  xy = geom.Point2D(x, y)
649 
650  # We put these two PSF calculations within this same try block so that we catch cases
651  # where the object's position is outside of the image.
652  try:
653  correctedFlux = psf.computeApertureFlux(self.config.calibFluxRadius, xy)
654  psfKernel = psf.computeKernelImage(xy).getArray()
655  psfKernel /= correctedFlux
656 
657  except InvalidParameterError:
658  self.log.info("%s at %0.4f, %0.4f outside of image" % (sourceType, x, y))
659  continue
660 
661  psfIm = galsim.InterpolatedImage(galsim.Image(psfKernel), scale=pixelScale)
662  galsimIm = galsim.InterpolatedImage(galsim.Image(im.array), scale=pixelScale)
663  convIm = galsim.Convolve([galsimIm, psfIm])
664 
665  try:
666  outIm = convIm.drawImage(scale=pixelScale, method="real_space").array
667  except (galsim.errors.GalSimFFTSizeError, MemoryError):
668  continue
669 
670  imSum = np.sum(outIm)
671  divIm = outIm/imSum
672 
673  try:
674  flux = photoCalib.magnitudeToInstFlux(mag, xy)
675  except LogicError:
676  flux = 0
677 
678  imWithFlux = flux*divIm
679 
680  if sourceType == b"galaxy":
681  galImages.append((afwImage.ImageF(imWithFlux), xy))
682  if sourceType == b"star":
683  starImages.append((afwImage.ImageF(imWithFlux), xy))
684 
685  return galImages, starImages
686 
687  def addPixCoords(self, fakeCat, image):
688 
689  """Add pixel coordinates to the catalog of fakes.
690 
691  Parameters
692  ----------
693  fakeCat : `pandas.core.frame.DataFrame`
694  The catalog of fake sources to be input
695  image : `lsst.afw.image.exposure.exposure.ExposureF`
696  The image into which the fake sources should be added
697 
698  Returns
699  -------
700  fakeCat : `pandas.core.frame.DataFrame`
701  """
702  wcs = image.getWcs()
703  ras = fakeCat[self.config.raColName].values
704  decs = fakeCat[self.config.decColName].values
705  xs, ys = wcs.skyToPixelArray(ras, decs)
706  fakeCat["x"] = xs
707  fakeCat["y"] = ys
708 
709  return fakeCat
710 
711  def trimFakeCat(self, fakeCat, image):
712  """Trim the fake cat to about the size of the input image.
713 
714  `fakeCat` must be processed with addPixCoords before using this method.
715 
716  Parameters
717  ----------
718  fakeCat : `pandas.core.frame.DataFrame`
719  The catalog of fake sources to be input
720  image : `lsst.afw.image.exposure.exposure.ExposureF`
721  The image into which the fake sources should be added
722 
723  Returns
724  -------
725  fakeCat : `pandas.core.frame.DataFrame`
726  The original fakeCat trimmed to the area of the image
727  """
728 
729  bbox = Box2D(image.getBBox()).dilatedBy(self.config.trimBuffer)
730  xs = fakeCat["x"].values
731  ys = fakeCat["y"].values
732 
733  isContained = xs >= bbox.minX
734  isContained &= xs <= bbox.maxX
735  isContained &= ys >= bbox.minY
736  isContained &= ys <= bbox.maxY
737 
738  return fakeCat[isContained]
739 
740  def mkFakeGalsimGalaxies(self, fakeCat, band, photoCalib, pixelScale, psf, image):
741  """Make images of fake galaxies using GalSim.
742 
743  Parameters
744  ----------
745  band : `str`
746  pixelScale : `float`
747  psf : `lsst.meas.extensions.psfex.psfexPsf.PsfexPsf`
748  The PSF information to use to make the PSF images
749  fakeCat : `pandas.core.frame.DataFrame`
750  The catalog of fake sources to be input
751  photoCalib : `lsst.afw.image.photoCalib.PhotoCalib`
752  Photometric calibration to be used to calibrate the fake sources
753 
754  Yields
755  -------
756  galImages : `generator`
757  A generator of tuples of `lsst.afw.image.exposure.exposure.ExposureF` and
758  `lsst.geom.Point2D` of their locations.
759 
760  Notes
761  -----
762 
763  Fake galaxies are made by combining two sersic profiles, one for the bulge and one for the disk. Each
764  component has an individual sersic index (n), a, b and position angle (PA). The combined profile is
765  then convolved with the PSF at the specified x, y position on the image.
766 
767  The names of the columns in the ``fakeCat`` are configurable and are the column names from the
768  University of Washington simulations database as default. For more information see the doc strings
769  attached to the config options.
770 
771  See mkFakeStars doc string for an explanation of calibration to instrumental flux.
772  """
773 
774  self.log.info("Making %d fake galaxy images" % len(fakeCat))
775 
776  for (index, row) in fakeCat.iterrows():
777  xy = geom.Point2D(row["x"], row["y"])
778 
779  # We put these two PSF calculations within this same try block so that we catch cases
780  # where the object's position is outside of the image.
781  try:
782  correctedFlux = psf.computeApertureFlux(self.config.calibFluxRadius, xy)
783  psfKernel = psf.computeKernelImage(xy).getArray()
784  psfKernel /= correctedFlux
785 
786  except InvalidParameterError:
787  self.log.info("Galaxy at %0.4f, %0.4f outside of image" % (row["x"], row["y"]))
788  continue
789 
790  try:
791  flux = photoCalib.magnitudeToInstFlux(row[self.config.magVar % band], xy)
792  except LogicError:
793  flux = 0
794 
795  bulge = galsim.Sersic(row[self.config.nBulge], half_light_radius=row[self.config.bulgeHLR])
796  axisRatioBulge = row[self.config.bBulge]/row[self.config.aBulge]
797  bulge = bulge.shear(q=axisRatioBulge, beta=((90 - row[self.config.paBulge])*galsim.degrees))
798 
799  disk = galsim.Sersic(row[self.config.nDisk], half_light_radius=row[self.config.diskHLR])
800  axisRatioDisk = row[self.config.bDisk]/row[self.config.aDisk]
801  disk = disk.shear(q=axisRatioDisk, beta=((90 - row[self.config.paDisk])*galsim.degrees))
802 
803  gal = disk + bulge
804  gal = gal.withFlux(flux)
805 
806  psfIm = galsim.InterpolatedImage(galsim.Image(psfKernel), scale=pixelScale)
807  gal = galsim.Convolve([gal, psfIm])
808  try:
809  galIm = gal.drawImage(scale=pixelScale, method="real_space").array
810  except (galsim.errors.GalSimFFTSizeError, MemoryError):
811  continue
812 
813  yield (afwImage.ImageF(galIm), xy)
814 
815  def mkFakeStars(self, fakeCat, band, photoCalib, psf, image):
816 
817  """Make fake stars based off the properties in the fakeCat.
818 
819  Parameters
820  ----------
821  band : `str`
822  psf : `lsst.meas.extensions.psfex.psfexPsf.PsfexPsf`
823  The PSF information to use to make the PSF images
824  fakeCat : `pandas.core.frame.DataFrame`
825  The catalog of fake sources to be input
826  image : `lsst.afw.image.exposure.exposure.ExposureF`
827  The image into which the fake sources should be added
828  photoCalib : `lsst.afw.image.photoCalib.PhotoCalib`
829  Photometric calibration to be used to calibrate the fake sources
830 
831  Yields
832  -------
833  starImages : `generator`
834  A generator of tuples of `lsst.afw.image.ImageF` of fake stars and
835  `lsst.geom.Point2D` of their locations.
836 
837  Notes
838  -----
839  To take a given magnitude and translate to the number of counts in the image
840  we use photoCalib.magnitudeToInstFlux, which returns the instrumental flux for the
841  given calibration radius used in the photometric calibration step.
842  Thus `calibFluxRadius` should be set to this same radius so that we can normalize
843  the PSF model to the correct instrumental flux within calibFluxRadius.
844  """
845 
846  self.log.info("Making %d fake star images" % len(fakeCat))
847 
848  for (index, row) in fakeCat.iterrows():
849  xy = geom.Point2D(row["x"], row["y"])
850 
851  # We put these two PSF calculations within this same try block so that we catch cases
852  # where the object's position is outside of the image.
853  try:
854  correctedFlux = psf.computeApertureFlux(self.config.calibFluxRadius, xy)
855  starIm = psf.computeImage(xy)
856  starIm /= correctedFlux
857 
858  except InvalidParameterError:
859  self.log.info("Star at %0.4f, %0.4f outside of image" % (row["x"], row["y"]))
860  continue
861 
862  try:
863  flux = photoCalib.magnitudeToInstFlux(row[self.config.magVar % band], xy)
864  except LogicError:
865  flux = 0
866 
867  starIm *= flux
868  yield ((starIm.convertF(), xy))
869 
870  def cleanCat(self, fakeCat, starCheckVal):
871  """Remove rows from the fakes catalog which have HLR = 0 for either the buldge or disk component,
872  also remove galaxies that have Sersic index outside the galsim min and max
873  allowed (0.3 <= n <= 6.2).
874 
875  Parameters
876  ----------
877  fakeCat : `pandas.core.frame.DataFrame`
878  The catalog of fake sources to be input
879  starCheckVal : `str`, `bytes` or `int`
880  The value that is set in the sourceType column to specifiy an object is a star.
881 
882  Returns
883  -------
884  fakeCat : `pandas.core.frame.DataFrame`
885  The input catalog of fake sources but with the bad objects removed
886 
887  Notes
888  -----
889  If the config option sourceSelectionColName is set then only objects with this column set to True
890  will be added.
891  """
892 
893  rowsToKeep = (((fakeCat[self.config.bulgeHLR] != 0.0) & (fakeCat[self.config.diskHLR] != 0.0))
894  | (fakeCat[self.config.sourceType] == starCheckVal))
895  numRowsNotUsed = len(fakeCat) - len(np.where(rowsToKeep)[0])
896  self.log.info("Removing %d rows with HLR = 0 for either the bulge or disk" % numRowsNotUsed)
897  fakeCat = fakeCat[rowsToKeep]
898 
899  minN = galsim.Sersic._minimum_n
900  maxN = galsim.Sersic._maximum_n
901  rowsWithGoodSersic = (((fakeCat[self.config.nBulge] >= minN) & (fakeCat[self.config.nBulge] <= maxN)
902  & (fakeCat[self.config.nDisk] >= minN) & (fakeCat[self.config.nDisk] <= maxN))
903  | (fakeCat[self.config.sourceType] == starCheckVal))
904  numRowsNotUsed = len(fakeCat) - len(np.where(rowsWithGoodSersic)[0])
905  self.log.info("Removing %d rows of galaxies with nBulge or nDisk outside of %0.2f <= n <= %0.2f" %
906  (numRowsNotUsed, minN, maxN))
907  fakeCat = fakeCat[rowsWithGoodSersic]
908 
909  if self.config.doSubSelectSources:
910  try:
911  rowsSelected = (fakeCat[self.config.sourceSelectionColName])
912  except KeyError:
913  raise KeyError("Given column, %s, for source selection not found." %
914  self.config.sourceSelectionColName)
915  numRowsNotUsed = len(fakeCat) - len(rowsSelected)
916  self.log.info("Removing %d rows which were not designated as template sources" % numRowsNotUsed)
917  fakeCat = fakeCat[rowsSelected]
918 
919  return fakeCat
920 
921  def addFakeSources(self, image, fakeImages, sourceType):
922  """Add the fake sources to the given image
923 
924  Parameters
925  ----------
926  image : `lsst.afw.image.exposure.exposure.ExposureF`
927  The image into which the fake sources should be added
928  fakeImages : `typing.Iterator` [`tuple` ['lsst.afw.image.ImageF`, `lsst.geom.Point2d`]]
929  An iterator of tuples that contains (or generates) images of fake sources,
930  and the locations they are to be inserted at.
931  sourceType : `str`
932  The type (star/galaxy) of fake sources input
933 
934  Returns
935  -------
936  image : `lsst.afw.image.exposure.exposure.ExposureF`
937 
938  Notes
939  -----
940  Uses the x, y information in the ``fakeCat`` to position an image of the fake interpolated onto the
941  pixel grid of the image. Sets the ``FAKE`` mask plane for the pixels added with the fake source.
942  """
943 
944  imageBBox = image.getBBox()
945  imageMI = image.maskedImage
946 
947  for (fakeImage, xy) in fakeImages:
948  X0 = xy.getX() - fakeImage.getWidth()/2 + 0.5
949  Y0 = xy.getY() - fakeImage.getHeight()/2 + 0.5
950  self.log.debug("Adding fake source at %d, %d" % (xy.getX(), xy.getY()))
951  if sourceType == "galaxy":
952  interpFakeImage = afwMath.offsetImage(fakeImage, X0, Y0, "lanczos3")
953  else:
954  interpFakeImage = fakeImage
955 
956  interpFakeImBBox = interpFakeImage.getBBox()
957  interpFakeImBBox.clip(imageBBox)
958 
959  if interpFakeImBBox.getArea() > 0:
960  imageMIView = imageMI[interpFakeImBBox]
961  clippedFakeImage = interpFakeImage[interpFakeImBBox]
962  clippedFakeImageMI = afwImage.MaskedImageF(clippedFakeImage)
963  clippedFakeImageMI.mask.set(self.bitmask)
964  imageMIView += clippedFakeImageMI
965 
966  return image
967 
968  def _getMetadataName(self):
969  """Disable metadata writing"""
970  return None
def run(self, skyInfo, tempExpRefList, imageScalerList, weightList, altMaskList=None, mask=None, supplementaryData=None)
def addPixCoords(self, fakeCat, image)
Definition: insertFakes.py:687
def mkFakeStars(self, fakeCat, band, photoCalib, psf, image)
Definition: insertFakes.py:815
def mkFakeGalsimGalaxies(self, fakeCat, band, photoCalib, pixelScale, psf, image)
Definition: insertFakes.py:740
def cleanCat(self, fakeCat, starCheckVal)
Definition: insertFakes.py:870
def processImagesForInsertion(self, fakeCat, wcs, psf, photoCalib, band, pixelScale)
Definition: insertFakes.py:600
def trimFakeCat(self, fakeCat, image)
Definition: insertFakes.py:711
def addFakeSources(self, image, fakeImages, sourceType)
Definition: insertFakes.py:921