lsst.pipe.tasks  21.0.0-85-g296c1d01+237be6640c
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  image.mask.addMaskPlane("FAKE")
478  else:
479  raise RuntimeError("No fakes found for this dataRef.")
480 
481  # restore original exposure WCS and photoCalib
482  image.setWcs(origWcs)
483  image.setPhotoCalib(origPhotoCalib)
484 
485  resultStruct = pipeBase.Struct(imageWithFakes=image)
486 
487  return resultStruct
488 
489  def _generateGSObjectsFromCatalog(self, exposure, fakeCat, galCheckVal, starCheckVal):
490  """Process catalog to generate `galsim.GSObject` s.
491 
492  Parameters
493  ----------
494  exposure : `lsst.afw.image.exposure.exposure.ExposureF`
495  The exposure into which the fake sources should be added
496  fakeCat : `pandas.core.frame.DataFrame`
497  The catalog of fake sources to be input
498  galCheckVal : `str`, `bytes` or `int`
499  The value that is set in the sourceType column to specifiy an object is a galaxy.
500  starCheckVal : `str`, `bytes` or `int`
501  The value that is set in the sourceType column to specifiy an object is a star.
502 
503  Yields
504  ------
505  gsObjects : `generator`
506  A generator of tuples of `lsst.geom.SpherePoint` and `galsim.GSObject`.
507  """
508  band = exposure.getFilterLabel().bandLabel
509  wcs = exposure.getWcs()
510  photoCalib = exposure.getPhotoCalib()
511 
512  self.log.info(f"Making {len(fakeCat)} objects for insertion")
513 
514  for (index, row) in fakeCat.iterrows():
515  ra = row[self.config.raColName]
516  dec = row[self.config.decColName]
517  skyCoord = SpherePoint(ra, dec, radians)
518  xy = wcs.skyToPixel(skyCoord)
519 
520  try:
521  flux = photoCalib.magnitudeToInstFlux(row[self.config.magVar % band], xy)
522  except LogicError:
523  continue
524 
525  sourceType = row[self.config.sourceType]
526  if sourceType == galCheckVal:
527  bulge = galsim.Sersic(n=row[self.config.nBulge], half_light_radius=row[self.config.bulgeHLR])
528  axisRatioBulge = row[self.config.bBulge]/row[self.config.aBulge]
529  bulge = bulge.shear(q=axisRatioBulge, beta=((90 - row[self.config.paBulge])*galsim.degrees))
530 
531  disk = galsim.Sersic(n=row[self.config.nDisk], half_light_radius=row[self.config.diskHLR])
532  axisRatioDisk = row[self.config.bDisk]/row[self.config.aDisk]
533  disk = disk.shear(q=axisRatioDisk, beta=((90 - row[self.config.paDisk])*galsim.degrees))
534 
535  gal = bulge + disk
536  gal = gal.withFlux(flux)
537 
538  yield skyCoord, gal
539  elif sourceType == starCheckVal:
540  star = galsim.DeltaFunction()
541  star = star.withFlux(flux)
542  yield skyCoord, star
543  else:
544  raise TypeError(f"Unknown sourceType {sourceType}")
545 
546  def _generateGSObjectsFromImages(self, exposure, fakeCat):
547  """Process catalog to generate `galsim.GSObject` s.
548 
549  Parameters
550  ----------
551  exposure : `lsst.afw.image.exposure.exposure.ExposureF`
552  The exposure into which the fake sources should be added
553  fakeCat : `pandas.core.frame.DataFrame`
554  The catalog of fake sources to be input
555 
556  Yields
557  ------
558  gsObjects : `generator`
559  A generator of tuples of `lsst.geom.SpherePoint` and `galsim.GSObject`.
560  """
561  band = exposure.getFilterLabel().bandLabel
562  wcs = exposure.getWcs()
563  photoCalib = exposure.getPhotoCalib()
564 
565  self.log.info(f"Processing {len(fakeCat)} fake images")
566 
567  for (index, row) in fakeCat.iterrows():
568  ra = row[self.config.raColName]
569  dec = row[self.config.decColName]
570  skyCoord = SpherePoint(ra, dec, radians)
571  xy = wcs.skyToPixel(skyCoord)
572 
573  try:
574  flux = photoCalib.magnitudeToInstFlux(row[self.config.magVar % band], xy)
575  except LogicError:
576  continue
577 
578  imFile = row[band+"imFilename"]
579  try:
580  imFile = imFile.decode("utf-8")
581  except AttributeError:
582  pass
583  imFile = imFile.strip()
584  im = galsim.fits.read(imFile, read_header=True)
585 
586  # GalSim will always attach a WCS to the image read in as above. If
587  # it can't find a WCS in the header, then it defaults to scale = 1.0
588  # arcsec / pix. So if that's the scale, then we need to check if it
589  # was explicitly set or if it's just the default. If it's just the
590  # default then we should override with the pixel scale of the target
591  # image.
592  if _isWCSGalsimDefault(im.wcs, im.header):
593  im.wcs = galsim.PixelScale(
594  wcs.getPixelScale().asArcseconds()
595  )
596 
597  obj = galsim.InterpolatedImage(im)
598  obj = obj.withFlux(flux)
599  yield skyCoord, obj
600 
601  def processImagesForInsertion(self, fakeCat, wcs, psf, photoCalib, band, pixelScale):
602  """Process images from files into the format needed for insertion.
603 
604  Parameters
605  ----------
606  fakeCat : `pandas.core.frame.DataFrame`
607  The catalog of fake sources to be input
608  wcs : `lsst.afw.geom.skyWcs.skyWcs.SkyWc`
609  WCS to use to add fake sources
610  psf : `lsst.meas.algorithms.coaddPsf.coaddPsf.CoaddPsf` or
611  `lsst.meas.extensions.psfex.psfexPsf.PsfexPsf`
612  The PSF information to use to make the PSF images
613  photoCalib : `lsst.afw.image.photoCalib.PhotoCalib`
614  Photometric calibration to be used to calibrate the fake sources
615  band : `str`
616  The filter band that the observation was taken in.
617  pixelScale : `float`
618  The pixel scale of the image the sources are to be added to.
619 
620  Returns
621  -------
622  galImages : `list`
623  A list of tuples of `lsst.afw.image.exposure.exposure.ExposureF` and
624  `lsst.geom.Point2D` of their locations.
625  For sources labelled as galaxy.
626  starImages : `list`
627  A list of tuples of `lsst.afw.image.exposure.exposure.ExposureF` and
628  `lsst.geom.Point2D` of their locations.
629  For sources labelled as star.
630 
631  Notes
632  -----
633  The input fakes catalog needs to contain the absolute path to the image in the
634  band that is being used to add images to. It also needs to have the R.A. and
635  declination of the fake source in radians and the sourceType of the object.
636  """
637  galImages = []
638  starImages = []
639 
640  self.log.info("Processing %d fake images" % len(fakeCat))
641 
642  for (imFile, sourceType, mag, x, y) in zip(fakeCat[band + "imFilename"].array,
643  fakeCat["sourceType"].array,
644  fakeCat[self.config.magVar % band].array,
645  fakeCat["x"].array, fakeCat["y"].array):
646 
647  im = afwImage.ImageF.readFits(imFile)
648 
649  xy = geom.Point2D(x, y)
650 
651  # We put these two PSF calculations within this same try block so that we catch cases
652  # where the object's position is outside of the image.
653  try:
654  correctedFlux = psf.computeApertureFlux(self.config.calibFluxRadius, xy)
655  psfKernel = psf.computeKernelImage(xy).getArray()
656  psfKernel /= correctedFlux
657 
658  except InvalidParameterError:
659  self.log.info("%s at %0.4f, %0.4f outside of image" % (sourceType, x, y))
660  continue
661 
662  psfIm = galsim.InterpolatedImage(galsim.Image(psfKernel), scale=pixelScale)
663  galsimIm = galsim.InterpolatedImage(galsim.Image(im.array), scale=pixelScale)
664  convIm = galsim.Convolve([galsimIm, psfIm])
665 
666  try:
667  outIm = convIm.drawImage(scale=pixelScale, method="real_space").array
668  except (galsim.errors.GalSimFFTSizeError, MemoryError):
669  continue
670 
671  imSum = np.sum(outIm)
672  divIm = outIm/imSum
673 
674  try:
675  flux = photoCalib.magnitudeToInstFlux(mag, xy)
676  except LogicError:
677  flux = 0
678 
679  imWithFlux = flux*divIm
680 
681  if sourceType == b"galaxy":
682  galImages.append((afwImage.ImageF(imWithFlux), xy))
683  if sourceType == b"star":
684  starImages.append((afwImage.ImageF(imWithFlux), xy))
685 
686  return galImages, starImages
687 
688  def addPixCoords(self, fakeCat, image):
689 
690  """Add pixel coordinates to the catalog of fakes.
691 
692  Parameters
693  ----------
694  fakeCat : `pandas.core.frame.DataFrame`
695  The catalog of fake sources to be input
696  image : `lsst.afw.image.exposure.exposure.ExposureF`
697  The image into which the fake sources should be added
698 
699  Returns
700  -------
701  fakeCat : `pandas.core.frame.DataFrame`
702  """
703  wcs = image.getWcs()
704  ras = fakeCat[self.config.raColName].values
705  decs = fakeCat[self.config.decColName].values
706  xs, ys = wcs.skyToPixelArray(ras, decs)
707  fakeCat["x"] = xs
708  fakeCat["y"] = ys
709 
710  return fakeCat
711 
712  def trimFakeCat(self, fakeCat, image):
713  """Trim the fake cat to about the size of the input image.
714 
715  `fakeCat` must be processed with addPixCoords before using this method.
716 
717  Parameters
718  ----------
719  fakeCat : `pandas.core.frame.DataFrame`
720  The catalog of fake sources to be input
721  image : `lsst.afw.image.exposure.exposure.ExposureF`
722  The image into which the fake sources should be added
723 
724  Returns
725  -------
726  fakeCat : `pandas.core.frame.DataFrame`
727  The original fakeCat trimmed to the area of the image
728  """
729 
730  bbox = Box2D(image.getBBox()).dilatedBy(self.config.trimBuffer)
731  xs = fakeCat["x"].values
732  ys = fakeCat["y"].values
733 
734  isContained = xs >= bbox.minX
735  isContained &= xs <= bbox.maxX
736  isContained &= ys >= bbox.minY
737  isContained &= ys <= bbox.maxY
738 
739  return fakeCat[isContained]
740 
741  def mkFakeGalsimGalaxies(self, fakeCat, band, photoCalib, pixelScale, psf, image):
742  """Make images of fake galaxies using GalSim.
743 
744  Parameters
745  ----------
746  band : `str`
747  pixelScale : `float`
748  psf : `lsst.meas.extensions.psfex.psfexPsf.PsfexPsf`
749  The PSF information to use to make the PSF images
750  fakeCat : `pandas.core.frame.DataFrame`
751  The catalog of fake sources to be input
752  photoCalib : `lsst.afw.image.photoCalib.PhotoCalib`
753  Photometric calibration to be used to calibrate the fake sources
754 
755  Yields
756  -------
757  galImages : `generator`
758  A generator of tuples of `lsst.afw.image.exposure.exposure.ExposureF` and
759  `lsst.geom.Point2D` of their locations.
760 
761  Notes
762  -----
763 
764  Fake galaxies are made by combining two sersic profiles, one for the bulge and one for the disk. Each
765  component has an individual sersic index (n), a, b and position angle (PA). The combined profile is
766  then convolved with the PSF at the specified x, y position on the image.
767 
768  The names of the columns in the ``fakeCat`` are configurable and are the column names from the
769  University of Washington simulations database as default. For more information see the doc strings
770  attached to the config options.
771 
772  See mkFakeStars doc string for an explanation of calibration to instrumental flux.
773  """
774 
775  self.log.info("Making %d fake galaxy images" % len(fakeCat))
776 
777  for (index, row) in fakeCat.iterrows():
778  xy = geom.Point2D(row["x"], row["y"])
779 
780  # We put these two PSF calculations within this same try block so that we catch cases
781  # where the object's position is outside of the image.
782  try:
783  correctedFlux = psf.computeApertureFlux(self.config.calibFluxRadius, xy)
784  psfKernel = psf.computeKernelImage(xy).getArray()
785  psfKernel /= correctedFlux
786 
787  except InvalidParameterError:
788  self.log.info("Galaxy at %0.4f, %0.4f outside of image" % (row["x"], row["y"]))
789  continue
790 
791  try:
792  flux = photoCalib.magnitudeToInstFlux(row[self.config.magVar % band], xy)
793  except LogicError:
794  flux = 0
795 
796  bulge = galsim.Sersic(row[self.config.nBulge], half_light_radius=row[self.config.bulgeHLR])
797  axisRatioBulge = row[self.config.bBulge]/row[self.config.aBulge]
798  bulge = bulge.shear(q=axisRatioBulge, beta=((90 - row[self.config.paBulge])*galsim.degrees))
799 
800  disk = galsim.Sersic(row[self.config.nDisk], half_light_radius=row[self.config.diskHLR])
801  axisRatioDisk = row[self.config.bDisk]/row[self.config.aDisk]
802  disk = disk.shear(q=axisRatioDisk, beta=((90 - row[self.config.paDisk])*galsim.degrees))
803 
804  gal = disk + bulge
805  gal = gal.withFlux(flux)
806 
807  psfIm = galsim.InterpolatedImage(galsim.Image(psfKernel), scale=pixelScale)
808  gal = galsim.Convolve([gal, psfIm])
809  try:
810  galIm = gal.drawImage(scale=pixelScale, method="real_space").array
811  except (galsim.errors.GalSimFFTSizeError, MemoryError):
812  continue
813 
814  yield (afwImage.ImageF(galIm), xy)
815 
816  def mkFakeStars(self, fakeCat, band, photoCalib, psf, image):
817 
818  """Make fake stars based off the properties in the fakeCat.
819 
820  Parameters
821  ----------
822  band : `str`
823  psf : `lsst.meas.extensions.psfex.psfexPsf.PsfexPsf`
824  The PSF information to use to make the PSF images
825  fakeCat : `pandas.core.frame.DataFrame`
826  The catalog of fake sources to be input
827  image : `lsst.afw.image.exposure.exposure.ExposureF`
828  The image into which the fake sources should be added
829  photoCalib : `lsst.afw.image.photoCalib.PhotoCalib`
830  Photometric calibration to be used to calibrate the fake sources
831 
832  Yields
833  -------
834  starImages : `generator`
835  A generator of tuples of `lsst.afw.image.ImageF` of fake stars and
836  `lsst.geom.Point2D` of their locations.
837 
838  Notes
839  -----
840  To take a given magnitude and translate to the number of counts in the image
841  we use photoCalib.magnitudeToInstFlux, which returns the instrumental flux for the
842  given calibration radius used in the photometric calibration step.
843  Thus `calibFluxRadius` should be set to this same radius so that we can normalize
844  the PSF model to the correct instrumental flux within calibFluxRadius.
845  """
846 
847  self.log.info("Making %d fake star images" % len(fakeCat))
848 
849  for (index, row) in fakeCat.iterrows():
850  xy = geom.Point2D(row["x"], row["y"])
851 
852  # We put these two PSF calculations within this same try block so that we catch cases
853  # where the object's position is outside of the image.
854  try:
855  correctedFlux = psf.computeApertureFlux(self.config.calibFluxRadius, xy)
856  starIm = psf.computeImage(xy)
857  starIm /= correctedFlux
858 
859  except InvalidParameterError:
860  self.log.info("Star at %0.4f, %0.4f outside of image" % (row["x"], row["y"]))
861  continue
862 
863  try:
864  flux = photoCalib.magnitudeToInstFlux(row[self.config.magVar % band], xy)
865  except LogicError:
866  flux = 0
867 
868  starIm *= flux
869  yield ((starIm.convertF(), xy))
870 
871  def cleanCat(self, fakeCat, starCheckVal):
872  """Remove rows from the fakes catalog which have HLR = 0 for either the buldge or disk component,
873  also remove galaxies that have Sersic index outside the galsim min and max
874  allowed (0.3 <= n <= 6.2).
875 
876  Parameters
877  ----------
878  fakeCat : `pandas.core.frame.DataFrame`
879  The catalog of fake sources to be input
880  starCheckVal : `str`, `bytes` or `int`
881  The value that is set in the sourceType column to specifiy an object is a star.
882 
883  Returns
884  -------
885  fakeCat : `pandas.core.frame.DataFrame`
886  The input catalog of fake sources but with the bad objects removed
887 
888  Notes
889  -----
890  If the config option sourceSelectionColName is set then only objects with this column set to True
891  will be added.
892  """
893 
894  rowsToKeep = (((fakeCat[self.config.bulgeHLR] != 0.0) & (fakeCat[self.config.diskHLR] != 0.0))
895  | (fakeCat[self.config.sourceType] == starCheckVal))
896  numRowsNotUsed = len(fakeCat) - len(np.where(rowsToKeep)[0])
897  self.log.info("Removing %d rows with HLR = 0 for either the bulge or disk" % numRowsNotUsed)
898  fakeCat = fakeCat[rowsToKeep]
899 
900  minN = galsim.Sersic._minimum_n
901  maxN = galsim.Sersic._maximum_n
902  rowsWithGoodSersic = (((fakeCat[self.config.nBulge] >= minN) & (fakeCat[self.config.nBulge] <= maxN)
903  & (fakeCat[self.config.nDisk] >= minN) & (fakeCat[self.config.nDisk] <= maxN))
904  | (fakeCat[self.config.sourceType] == starCheckVal))
905  numRowsNotUsed = len(fakeCat) - len(np.where(rowsWithGoodSersic)[0])
906  self.log.info("Removing %d rows of galaxies with nBulge or nDisk outside of %0.2f <= n <= %0.2f" %
907  (numRowsNotUsed, minN, maxN))
908  fakeCat = fakeCat[rowsWithGoodSersic]
909 
910  if self.config.doSubSelectSources:
911  try:
912  rowsSelected = (fakeCat[self.config.sourceSelectionColName])
913  except KeyError:
914  raise KeyError("Given column, %s, for source selection not found." %
915  self.config.sourceSelectionColName)
916  numRowsNotUsed = len(fakeCat) - len(rowsSelected)
917  self.log.info("Removing %d rows which were not designated as template sources" % numRowsNotUsed)
918  fakeCat = fakeCat[rowsSelected]
919 
920  return fakeCat
921 
922  def addFakeSources(self, image, fakeImages, sourceType):
923  """Add the fake sources to the given image
924 
925  Parameters
926  ----------
927  image : `lsst.afw.image.exposure.exposure.ExposureF`
928  The image into which the fake sources should be added
929  fakeImages : `typing.Iterator` [`tuple` ['lsst.afw.image.ImageF`, `lsst.geom.Point2d`]]
930  An iterator of tuples that contains (or generates) images of fake sources,
931  and the locations they are to be inserted at.
932  sourceType : `str`
933  The type (star/galaxy) of fake sources input
934 
935  Returns
936  -------
937  image : `lsst.afw.image.exposure.exposure.ExposureF`
938 
939  Notes
940  -----
941  Uses the x, y information in the ``fakeCat`` to position an image of the fake interpolated onto the
942  pixel grid of the image. Sets the ``FAKE`` mask plane for the pixels added with the fake source.
943  """
944 
945  imageBBox = image.getBBox()
946  imageMI = image.maskedImage
947 
948  for (fakeImage, xy) in fakeImages:
949  X0 = xy.getX() - fakeImage.getWidth()/2 + 0.5
950  Y0 = xy.getY() - fakeImage.getHeight()/2 + 0.5
951  self.log.debug("Adding fake source at %d, %d" % (xy.getX(), xy.getY()))
952  if sourceType == "galaxy":
953  interpFakeImage = afwMath.offsetImage(fakeImage, X0, Y0, "lanczos3")
954  else:
955  interpFakeImage = fakeImage
956 
957  interpFakeImBBox = interpFakeImage.getBBox()
958  interpFakeImBBox.clip(imageBBox)
959 
960  if interpFakeImBBox.getArea() > 0:
961  imageMIView = imageMI[interpFakeImBBox]
962  clippedFakeImage = interpFakeImage[interpFakeImBBox]
963  clippedFakeImageMI = afwImage.MaskedImageF(clippedFakeImage)
964  clippedFakeImageMI.mask.set(self.bitmask)
965  imageMIView += clippedFakeImageMI
966 
967  return image
968 
969  def _getMetadataName(self):
970  """Disable metadata writing"""
971  return None
def run(self, skyInfo, tempExpRefList, imageScalerList, weightList, altMaskList=None, mask=None, supplementaryData=None)
def addPixCoords(self, fakeCat, image)
Definition: insertFakes.py:688
def mkFakeStars(self, fakeCat, band, photoCalib, psf, image)
Definition: insertFakes.py:816
def mkFakeGalsimGalaxies(self, fakeCat, band, photoCalib, pixelScale, psf, image)
Definition: insertFakes.py:741
def cleanCat(self, fakeCat, starCheckVal)
Definition: insertFakes.py:871
def processImagesForInsertion(self, fakeCat, wcs, psf, photoCalib, band, pixelScale)
Definition: insertFakes.py:601
def trimFakeCat(self, fakeCat, image)
Definition: insertFakes.py:712
def addFakeSources(self, image, fakeImages, sourceType)
Definition: insertFakes.py:922