lsst.pipe.tasks  21.0.0-100-g362f0c00+4647f1f7f0
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
36 import lsst.pipe.base.connectionTypes as cT
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 
180  # Unchanged
181 
182  doCleanCat = pexConfig.Field(
183  doc="If true removes bad sources from the catalog.",
184  dtype=bool,
185  default=True,
186  )
187 
188  fakeType = pexConfig.Field(
189  doc="What type of fake catalog to use, snapshot (includes variability in the magnitudes calculated "
190  "from the MJD of the image), static (no variability) or filename for a user defined fits"
191  "catalog.",
192  dtype=str,
193  default="static",
194  )
195 
196  calibFluxRadius = pexConfig.Field(
197  doc="Aperture radius (in pixels) that was used to define the calibration for this image+catalog. "
198  "This will be used to produce the correct instrumental fluxes within the radius. "
199  "This value should match that of the field defined in slot_CalibFlux_instFlux.",
200  dtype=float,
201  default=12.0,
202  )
203 
204  coaddName = pexConfig.Field(
205  doc="The name of the type of coadd used",
206  dtype=str,
207  default="deep",
208  )
209 
210  doSubSelectSources = pexConfig.Field(
211  doc="Set to True if you wish to sub select sources to be input based on the value in the column"
212  "set in the sourceSelectionColName config option.",
213  dtype=bool,
214  default=False
215  )
216 
217  insertImages = pexConfig.Field(
218  doc="Insert images directly? True or False.",
219  dtype=bool,
220  default=False,
221  )
222 
223  doProcessAllDataIds = pexConfig.Field(
224  doc="If True, all input data IDs will be processed, even those containing no fake sources.",
225  dtype=bool,
226  default=False,
227  )
228 
229  trimBuffer = pexConfig.Field(
230  doc="Size of the pixel buffer surrounding the image. Only those fake sources with a centroid"
231  "falling within the image+buffer region will be considered for fake source injection.",
232  dtype=int,
233  default=100,
234  )
235 
236  sourceType = pexConfig.Field(
237  doc="The column name for the source type used in the fake source catalog.",
238  dtype=str,
239  default="sourceType",
240  )
241 
242  # New source catalog config variables
243 
244  ra_col = pexConfig.Field(
245  doc="Source catalog column name for RA (in radians).",
246  dtype=str,
247  default="ra",
248  )
249 
250  dec_col = pexConfig.Field(
251  doc="Source catalog column name for dec (in radians).",
252  dtype=str,
253  default="dec",
254  )
255 
256  bulge_semimajor_col = pexConfig.Field(
257  doc="Source catalog column name for the semimajor axis (in arcseconds) "
258  "of the bulge half-light ellipse.",
259  dtype=str,
260  default="bulge_semimajor",
261  )
262 
263  bulge_axis_ratio_col = pexConfig.Field(
264  doc="Source catalog column name for the axis ratio of the bulge "
265  "half-light ellipse.",
266  dtype=str,
267  default="bulge_axis_ratio",
268  )
269 
270  bulge_pa_col = pexConfig.Field(
271  doc="Source catalog column name for the position angle (measured from "
272  "North through East in degrees) of the semimajor axis of the bulge "
273  "half-light ellipse.",
274  dtype=str,
275  default="bulge_pa",
276  )
277 
278  bulge_n_col = pexConfig.Field(
279  doc="Source catalog column name for the Sersic index of the bulge.",
280  dtype=str,
281  default="bulge_n",
282  )
283 
284  disk_semimajor_col = pexConfig.Field(
285  doc="Source catalog column name for the semimajor axis (in arcseconds) "
286  "of the disk half-light ellipse.",
287  dtype=str,
288  default="disk_semimajor",
289  )
290 
291  disk_axis_ratio_col = pexConfig.Field(
292  doc="Source catalog column name for the axis ratio of the disk "
293  "half-light ellipse.",
294  dtype=str,
295  default="disk_axis_ratio",
296  )
297 
298  disk_pa_col = pexConfig.Field(
299  doc="Source catalog column name for the position angle (measured from "
300  "North through East in degrees) of the semimajor axis of the disk "
301  "half-light ellipse.",
302  dtype=str,
303  default="disk_pa",
304  )
305 
306  disk_n_col = pexConfig.Field(
307  doc="Source catalog column name for the Sersic index of the disk.",
308  dtype=str,
309  default="disk_n",
310  )
311 
312  bulge_disk_flux_ratio_col = pexConfig.Field(
313  doc="Source catalog column name for the bulge/disk flux ratio.",
314  dtype=str,
315  default="bulge_disk_flux_ratio",
316  )
317 
318  mag_col = pexConfig.Field(
319  doc="Source catalog column name template for magnitudes, in the format "
320  "``filter name``_mag_col. E.g., if this config variable is set to "
321  "``%s_mag``, then the i-band magnitude will be searched for in the "
322  "``i_mag`` column of the source catalog.",
323  dtype=str,
324  default="%s_mag"
325  )
326 
327  select_col = pexConfig.Field(
328  doc="Source catalog column name to be used to select which sources to "
329  "add.",
330  dtype=str,
331  default="select",
332  )
333 
334  # Deprecated config variables
335 
336  raColName = pexConfig.Field(
337  doc="RA column name used in the fake source catalog.",
338  dtype=str,
339  default="raJ2000",
340  deprecated="Use `ra_col` instead."
341  )
342 
343  decColName = pexConfig.Field(
344  doc="Dec. column name used in the fake source catalog.",
345  dtype=str,
346  default="decJ2000",
347  deprecated="Use `dec_col` instead."
348  )
349 
350  diskHLR = pexConfig.Field(
351  doc="Column name for the disk half light radius used in the fake source catalog.",
352  dtype=str,
353  default="DiskHalfLightRadius",
354  deprecated=(
355  "Use `disk_semimajor_col`, `disk_axis_ratio_col`, and `disk_pa_col`"
356  " to specify disk half-light ellipse."
357  )
358  )
359 
360  aDisk = pexConfig.Field(
361  doc="The column name for the semi major axis length of the disk component used in the fake source"
362  "catalog.",
363  dtype=str,
364  default="a_d",
365  deprecated=(
366  "Use `disk_semimajor_col`, `disk_axis_ratio_col`, and `disk_pa_col`"
367  " to specify disk half-light ellipse."
368  )
369  )
370 
371  bDisk = pexConfig.Field(
372  doc="The column name for the semi minor axis length of the disk component.",
373  dtype=str,
374  default="b_d",
375  deprecated=(
376  "Use `disk_semimajor_col`, `disk_axis_ratio_col`, and `disk_pa_col`"
377  " to specify disk half-light ellipse."
378  )
379  )
380 
381  paDisk = pexConfig.Field(
382  doc="The column name for the PA of the disk component used in the fake source catalog.",
383  dtype=str,
384  default="pa_disk",
385  deprecated=(
386  "Use `disk_semimajor_col`, `disk_axis_ratio_col`, and `disk_pa_col`"
387  " to specify disk half-light ellipse."
388  )
389  )
390 
391  nDisk = pexConfig.Field(
392  doc="The column name for the sersic index of the disk component used in the fake source catalog.",
393  dtype=str,
394  default="disk_n",
395  deprecated="Use `disk_n` instead."
396  )
397 
398  bulgeHLR = pexConfig.Field(
399  doc="Column name for the bulge half light radius used in the fake source catalog.",
400  dtype=str,
401  default="BulgeHalfLightRadius",
402  deprecated=(
403  "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and "
404  "`bulge_pa_col` to specify disk half-light ellipse."
405  )
406  )
407 
408  aBulge = pexConfig.Field(
409  doc="The column name for the semi major axis length of the bulge component.",
410  dtype=str,
411  default="a_b",
412  deprecated=(
413  "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and "
414  "`bulge_pa_col` to specify disk half-light ellipse."
415  )
416  )
417 
418  bBulge = pexConfig.Field(
419  doc="The column name for the semi minor axis length of the bulge component used in the fake source "
420  "catalog.",
421  dtype=str,
422  default="b_b",
423  deprecated=(
424  "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and "
425  "`bulge_pa_col` to specify disk half-light ellipse."
426  )
427  )
428 
429  paBulge = pexConfig.Field(
430  doc="The column name for the PA of the bulge component used in the fake source catalog.",
431  dtype=str,
432  default="pa_bulge",
433  deprecated=(
434  "Use `bulge_semimajor_col`, `bulge_axis_ratio_col`, and "
435  "`bulge_pa_col` to specify disk half-light ellipse."
436  )
437  )
438 
439  nBulge = pexConfig.Field(
440  doc="The column name for the sersic index of the bulge component used in the fake source catalog.",
441  dtype=str,
442  default="bulge_n",
443  deprecated="Use `bulge_n` instead."
444  )
445 
446  magVar = pexConfig.Field(
447  doc="The column name for the magnitude calculated taking variability into account. In the format "
448  "``filter name``magVar, e.g. imagVar for the magnitude in the i band.",
449  dtype=str,
450  default="%smagVar",
451  deprecated="Use `mag_col` instead."
452  )
453 
454  sourceSelectionColName = pexConfig.Field(
455  doc="The name of the column in the input fakes catalogue to be used to determine which sources to"
456  "add, default is none and when this is used all sources are added.",
457  dtype=str,
458  default="templateSource",
459  deprecated="Use `select_col` instead."
460  )
461 
462 
463 class InsertFakesTask(PipelineTask, CmdLineTask):
464  """Insert fake objects into images.
465 
466  Add fake stars and galaxies to the given image, read in through the dataRef. Galaxy parameters are read in
467  from the specified file and then modelled using galsim.
468 
469  `InsertFakesTask` has five functions that make images of the fake sources and then add them to the
470  image.
471 
472  `addPixCoords`
473  Use the WCS information to add the pixel coordinates of each source.
474  `mkFakeGalsimGalaxies`
475  Use Galsim to make fake double sersic galaxies for each set of galaxy parameters in the input file.
476  `mkFakeStars`
477  Use the PSF information from the image to make a fake star using the magnitude information from the
478  input file.
479  `cleanCat`
480  Remove rows of the input fake catalog which have half light radius, of either the bulge or the disk,
481  that are 0. Also removes rows that have Sersic index outside of galsim's allowed paramters. If
482  the config option sourceSelectionColName is set then this function limits the catalog of input fakes
483  to only those which are True in this column.
484  `addFakeSources`
485  Add the fake sources to the image.
486 
487  """
488 
489  _DefaultName = "insertFakes"
490  ConfigClass = InsertFakesConfig
491 
492  def runDataRef(self, dataRef):
493  """Read in/write out the required data products and add fake sources to the deepCoadd.
494 
495  Parameters
496  ----------
497  dataRef : `lsst.daf.persistence.butlerSubset.ButlerDataRef`
498  Data reference defining the image to have fakes added to it
499  Used to access the following data products:
500  deepCoadd
501  """
502 
503  infoStr = "Adding fakes to: tract: %d, patch: %s, filter: %s" % (dataRef.dataId["tract"],
504  dataRef.dataId["patch"],
505  dataRef.dataId["filter"])
506  self.log.info(infoStr)
507 
508  # To do: should it warn when asked to insert variable sources into the coadd
509 
510  if self.config.fakeType == "static":
511  fakeCat = dataRef.get("deepCoadd_fakeSourceCat").toDataFrame()
512  # To do: DM-16254, the read and write of the fake catalogs will be changed once the new pipeline
513  # task structure for ref cats is in place.
514  self.fakeSourceCatType = "deepCoadd_fakeSourceCat"
515  else:
516  fakeCat = Table.read(self.config.fakeType).to_pandas()
517 
518  coadd = dataRef.get("deepCoadd")
519  wcs = coadd.getWcs()
520  photoCalib = coadd.getPhotoCalib()
521 
522  imageWithFakes = self.run(fakeCat, coadd, wcs, photoCalib)
523 
524  dataRef.put(imageWithFakes.imageWithFakes, "fakes_deepCoadd")
525 
526  def runQuantum(self, butlerQC, inputRefs, outputRefs):
527  inputs = butlerQC.get(inputRefs)
528  inputs["wcs"] = inputs["image"].getWcs()
529  inputs["photoCalib"] = inputs["image"].getPhotoCalib()
530 
531  outputs = self.run(**inputs)
532  butlerQC.put(outputs, outputRefs)
533 
534  @classmethod
535  def _makeArgumentParser(cls):
536  parser = pipeBase.ArgumentParser(name=cls._DefaultName)
537  parser.add_id_argument(name="--id", datasetType="deepCoadd",
538  help="data IDs for the deepCoadd, e.g. --id tract=12345 patch=1,2 filter=r",
539  ContainerClass=ExistingCoaddDataIdContainer)
540  return parser
541 
542  def run(self, fakeCat, image, wcs, photoCalib):
543  """Add fake sources to an image.
544 
545  Parameters
546  ----------
547  fakeCat : `pandas.core.frame.DataFrame`
548  The catalog of fake sources to be input
549  image : `lsst.afw.image.exposure.exposure.ExposureF`
550  The image into which the fake sources should be added
551  wcs : `lsst.afw.geom.SkyWcs`
552  WCS to use to add fake sources
553  photoCalib : `lsst.afw.image.photoCalib.PhotoCalib`
554  Photometric calibration to be used to calibrate the fake sources
555 
556  Returns
557  -------
558  resultStruct : `lsst.pipe.base.struct.Struct`
559  contains : image : `lsst.afw.image.exposure.exposure.ExposureF`
560 
561  Notes
562  -----
563  Adds pixel coordinates for each source to the fakeCat and removes objects with bulge or disk half
564  light radius = 0 (if ``config.doCleanCat = True``).
565 
566  Adds the ``Fake`` mask plane to the image which is then set by `addFakeSources` to mark where fake
567  sources have been added. Uses the information in the ``fakeCat`` to make fake galaxies (using galsim)
568  and fake stars, using the PSF models from the PSF information for the image. These are then added to
569  the image and the image with fakes included returned.
570 
571  The galsim galaxies are made using a double sersic profile, one for the bulge and one for the disk,
572  this is then convolved with the PSF at that point.
573  """
574  # Attach overriding wcs and photoCalib to image, but retain originals
575  # so we can reset at the end.
576  origWcs = image.getWcs()
577  origPhotoCalib = image.getPhotoCalib()
578  image.setWcs(wcs)
579  image.setPhotoCalib(photoCalib)
580 
581  band = image.getFilterLabel().bandLabel
582  fakeCat = self._standardizeColumns(fakeCat, band)
583 
584  fakeCat = self.addPixCoords(fakeCat, image)
585  fakeCat = self.trimFakeCat(fakeCat, image)
586 
587  if len(fakeCat) > 0:
588  if isinstance(fakeCat[self.config.sourceType].iloc[0], str):
589  galCheckVal = "galaxy"
590  starCheckVal = "star"
591  elif isinstance(fakeCat[self.config.sourceType].iloc[0], bytes):
592  galCheckVal = b"galaxy"
593  starCheckVal = b"star"
594  elif isinstance(fakeCat[self.config.sourceType].iloc[0], (int, float)):
595  galCheckVal = 1
596  starCheckVal = 0
597  else:
598  raise TypeError("sourceType column does not have required type, should be str, bytes or int")
599 
600  if not self.config.insertImages:
601  if self.config.doCleanCat:
602  fakeCat = self.cleanCat(fakeCat, starCheckVal)
603 
604  generator = self._generateGSObjectsFromCatalog(image, fakeCat, galCheckVal, starCheckVal)
605  else:
606  generator = self._generateGSObjectsFromImages(image, fakeCat)
607  _add_fake_sources(image, generator, calibFluxRadius=self.config.calibFluxRadius, logger=self.log)
608  elif len(fakeCat) == 0 and self.config.doProcessAllDataIds:
609  self.log.warn("No fakes found for this dataRef; processing anyway.")
610  image.mask.addMaskPlane("FAKE")
611  else:
612  raise RuntimeError("No fakes found for this dataRef.")
613 
614  # restore original exposure WCS and photoCalib
615  image.setWcs(origWcs)
616  image.setPhotoCalib(origPhotoCalib)
617 
618  resultStruct = pipeBase.Struct(imageWithFakes=image)
619 
620  return resultStruct
621 
622  def _standardizeColumns(self, fakeCat, band):
623  """Use config variables to 'standardize' the expected columns and column
624  names in the input catalog.
625 
626  Parameters
627  ----------
628  fakeCat : `pandas.core.frame.DataFrame`
629  The catalog of fake sources to be input
630  band : `str`
631  Label for the current band being processed.
632 
633  Returns
634  -------
635  outCat : `pandas.core.frame.DataFrame`
636  The standardized catalog of fake sources
637  """
638  cfg = self.config
639  replace_dict = {}
640 
641  # Prefer new config variables over deprecated config variables.
642  # The following are fairly simple to handle as they're just column name
643  # changes.
644  for new_name, depr_name, std_name in [
645  (cfg.ra_col, cfg.raColName, 'ra'),
646  (cfg.dec_col, cfg.decColName, 'dec'),
647  (cfg.bulge_n_col, cfg.nBulge, 'bulge_n'),
648  (cfg.bulge_pa_col, cfg.paBulge, 'bulge_pa'),
649  (cfg.disk_n_col, cfg.nDisk, 'disk_n'),
650  (cfg.disk_pa_col, cfg.paDisk, 'disk_pa'),
651  (cfg.mag_col%band, cfg.magVar%band, 'mag'),
652  (cfg.select_col, cfg.sourceSelectionColName, 'select')
653  ]:
654  # Only standardize "select" column if doSubSelectSources is True
655  if not cfg.doSubSelectSources and std_name == 'select':
656  continue
657  if new_name in fakeCat.columns:
658  replace_dict[new_name] = std_name
659  elif depr_name in fakeCat.columns:
660  replace_dict[depr_name] = std_name
661  else:
662  raise ValueError(f"Could not determine column for {std_name}.")
663  fakeCat = fakeCat.rename(columns=replace_dict, copy=False)
664 
665  # Handling the half-light radius and axis-ratio are trickier, since we
666  # moved from expecting (HLR, a, b) to expecting (semimajor, axis_ratio).
667  # Just handle these manually.
668  if (
669  cfg.bulge_semimajor_col in fakeCat.columns
670  and cfg.bulge_axis_ratio_col in fakeCat.columns
671  ):
672  fakeCat = fakeCat.rename(
673  columns={
674  cfg.bulge_semimajor_col: 'bulge_semimajor',
675  cfg.bulge_axis_ratio_col: 'bulge_axis_ratio',
676  cfg.disk_semimajor_col: 'disk_semimajor',
677  cfg.disk_axis_ratio_col: 'disk_axis_ratio',
678  },
679  copy=False
680  )
681  elif (
682  cfg.bulgeHLR in fakeCat.columns
683  and cfg.aBulge in fakeCat.columns
684  and cfg.bBulge in fakeCat.columns
685  ):
686  fakeCat['bulge_axis_ratio'] = (
687  fakeCat[cfg.bBulge]/fakeCat[cfg.aBulge]
688  )
689  fakeCat['bulge_semimajor'] = (
690  fakeCat[cfg.bulgeHLR]/np.sqrt(fakeCat['bulge_axis_ratio'])
691  )
692  fakeCat['disk_axis_ratio'] = (
693  fakeCat[cfg.bDisk]/fakeCat[cfg.aDisk]
694  )
695  fakeCat['disk_semimajor'] = (
696  fakeCat[cfg.diskHLR]/np.sqrt(fakeCat['disk_axis_ratio'])
697  )
698  else:
699  raise ValueError(
700  "Could not determine columns for half-light radius and axis "
701  "ratio."
702  )
703 
704  # Process the bulge/disk flux ratio if possible.
705  if cfg.bulge_disk_flux_ratio_col in fakeCat.columns:
706  fakeCat = fakeCat.rename(
707  columns={
708  cfg.bulge_disk_flux_ratio_col: 'bulge_disk_flux_ratio'
709  },
710  copy=False
711  )
712  else:
713  fakeCat['bulge_disk_flux_ratio'] = 1.0
714 
715  return fakeCat
716 
717  def _generateGSObjectsFromCatalog(self, exposure, fakeCat, galCheckVal, starCheckVal):
718  """Process catalog to generate `galsim.GSObject` s.
719 
720  Parameters
721  ----------
722  exposure : `lsst.afw.image.exposure.exposure.ExposureF`
723  The exposure into which the fake sources should be added
724  fakeCat : `pandas.core.frame.DataFrame`
725  The catalog of fake sources to be input
726  galCheckVal : `str`, `bytes` or `int`
727  The value that is set in the sourceType column to specifiy an object is a galaxy.
728  starCheckVal : `str`, `bytes` or `int`
729  The value that is set in the sourceType column to specifiy an object is a star.
730 
731  Yields
732  ------
733  gsObjects : `generator`
734  A generator of tuples of `lsst.geom.SpherePoint` and `galsim.GSObject`.
735  """
736  wcs = exposure.getWcs()
737  photoCalib = exposure.getPhotoCalib()
738 
739  self.log.info(f"Making {len(fakeCat)} objects for insertion")
740 
741  for (index, row) in fakeCat.iterrows():
742  ra = row['ra']
743  dec = row['dec']
744  skyCoord = SpherePoint(ra, dec, radians)
745  xy = wcs.skyToPixel(skyCoord)
746 
747  try:
748  flux = photoCalib.magnitudeToInstFlux(row['mag'], xy)
749  except LogicError:
750  continue
751 
752  sourceType = row[self.config.sourceType]
753  if sourceType == galCheckVal:
754  # GalSim convention: HLR = sqrt(a * b) = a * sqrt(b / a)
755  bulge_gs_HLR = row['bulge_semimajor']*np.sqrt(row['bulge_axis_ratio'])
756  bulge = galsim.Sersic(n=row['bulge_n'], half_light_radius=bulge_gs_HLR)
757  bulge = bulge.shear(q=row['bulge_axis_ratio'], beta=((90 - row['bulge_pa'])*galsim.degrees))
758 
759  disk_gs_HLR = row['disk_semimajor']*np.sqrt(row['disk_axis_ratio'])
760  disk = galsim.Sersic(n=row['disk_n'], half_light_radius=disk_gs_HLR)
761  disk = disk.shear(q=row['disk_axis_ratio'], beta=((90 - row['disk_pa'])*galsim.degrees))
762 
763  gal = bulge*row['bulge_disk_flux_ratio'] + disk
764  gal = gal.withFlux(flux)
765 
766  yield skyCoord, gal
767  elif sourceType == starCheckVal:
768  star = galsim.DeltaFunction()
769  star = star.withFlux(flux)
770  yield skyCoord, star
771  else:
772  raise TypeError(f"Unknown sourceType {sourceType}")
773 
774  def _generateGSObjectsFromImages(self, exposure, fakeCat):
775  """Process catalog to generate `galsim.GSObject` s.
776 
777  Parameters
778  ----------
779  exposure : `lsst.afw.image.exposure.exposure.ExposureF`
780  The exposure into which the fake sources should be added
781  fakeCat : `pandas.core.frame.DataFrame`
782  The catalog of fake sources to be input
783 
784  Yields
785  ------
786  gsObjects : `generator`
787  A generator of tuples of `lsst.geom.SpherePoint` and `galsim.GSObject`.
788  """
789  band = exposure.getFilterLabel().bandLabel
790  wcs = exposure.getWcs()
791  photoCalib = exposure.getPhotoCalib()
792 
793  self.log.info(f"Processing {len(fakeCat)} fake images")
794 
795  for (index, row) in fakeCat.iterrows():
796  ra = row['ra']
797  dec = row['dec']
798  skyCoord = SpherePoint(ra, dec, radians)
799  xy = wcs.skyToPixel(skyCoord)
800 
801  try:
802  flux = photoCalib.magnitudeToInstFlux(row['mag'], xy)
803  except LogicError:
804  continue
805 
806  imFile = row[band+"imFilename"]
807  try:
808  imFile = imFile.decode("utf-8")
809  except AttributeError:
810  pass
811  imFile = imFile.strip()
812  im = galsim.fits.read(imFile, read_header=True)
813 
814  # GalSim will always attach a WCS to the image read in as above. If
815  # it can't find a WCS in the header, then it defaults to scale = 1.0
816  # arcsec / pix. So if that's the scale, then we need to check if it
817  # was explicitly set or if it's just the default. If it's just the
818  # default then we should override with the pixel scale of the target
819  # image.
820  if _isWCSGalsimDefault(im.wcs, im.header):
821  im.wcs = galsim.PixelScale(
822  wcs.getPixelScale().asArcseconds()
823  )
824 
825  obj = galsim.InterpolatedImage(im)
826  obj = obj.withFlux(flux)
827  yield skyCoord, obj
828 
829  def processImagesForInsertion(self, fakeCat, wcs, psf, photoCalib, band, pixelScale):
830  """Process images from files into the format needed for insertion.
831 
832  Parameters
833  ----------
834  fakeCat : `pandas.core.frame.DataFrame`
835  The catalog of fake sources to be input
836  wcs : `lsst.afw.geom.skyWcs.skyWcs.SkyWc`
837  WCS to use to add fake sources
838  psf : `lsst.meas.algorithms.coaddPsf.coaddPsf.CoaddPsf` or
839  `lsst.meas.extensions.psfex.psfexPsf.PsfexPsf`
840  The PSF information to use to make the PSF images
841  photoCalib : `lsst.afw.image.photoCalib.PhotoCalib`
842  Photometric calibration to be used to calibrate the fake sources
843  band : `str`
844  The filter band that the observation was taken in.
845  pixelScale : `float`
846  The pixel scale of the image the sources are to be added to.
847 
848  Returns
849  -------
850  galImages : `list`
851  A list of tuples of `lsst.afw.image.exposure.exposure.ExposureF` and
852  `lsst.geom.Point2D` of their locations.
853  For sources labelled as galaxy.
854  starImages : `list`
855  A list of tuples of `lsst.afw.image.exposure.exposure.ExposureF` and
856  `lsst.geom.Point2D` of their locations.
857  For sources labelled as star.
858 
859  Notes
860  -----
861  The input fakes catalog needs to contain the absolute path to the image in the
862  band that is being used to add images to. It also needs to have the R.A. and
863  declination of the fake source in radians and the sourceType of the object.
864  """
865  galImages = []
866  starImages = []
867 
868  self.log.info("Processing %d fake images" % len(fakeCat))
869 
870  for (imFile, sourceType, mag, x, y) in zip(fakeCat[band + "imFilename"].array,
871  fakeCat["sourceType"].array,
872  fakeCat['mag'].array,
873  fakeCat["x"].array, fakeCat["y"].array):
874 
875  im = afwImage.ImageF.readFits(imFile)
876 
877  xy = geom.Point2D(x, y)
878 
879  # We put these two PSF calculations within this same try block so that we catch cases
880  # where the object's position is outside of the image.
881  try:
882  correctedFlux = psf.computeApertureFlux(self.config.calibFluxRadius, xy)
883  psfKernel = psf.computeKernelImage(xy).getArray()
884  psfKernel /= correctedFlux
885 
886  except InvalidParameterError:
887  self.log.info("%s at %0.4f, %0.4f outside of image" % (sourceType, x, y))
888  continue
889 
890  psfIm = galsim.InterpolatedImage(galsim.Image(psfKernel), scale=pixelScale)
891  galsimIm = galsim.InterpolatedImage(galsim.Image(im.array), scale=pixelScale)
892  convIm = galsim.Convolve([galsimIm, psfIm])
893 
894  try:
895  outIm = convIm.drawImage(scale=pixelScale, method="real_space").array
896  except (galsim.errors.GalSimFFTSizeError, MemoryError):
897  continue
898 
899  imSum = np.sum(outIm)
900  divIm = outIm/imSum
901 
902  try:
903  flux = photoCalib.magnitudeToInstFlux(mag, xy)
904  except LogicError:
905  flux = 0
906 
907  imWithFlux = flux*divIm
908 
909  if sourceType == b"galaxy":
910  galImages.append((afwImage.ImageF(imWithFlux), xy))
911  if sourceType == b"star":
912  starImages.append((afwImage.ImageF(imWithFlux), xy))
913 
914  return galImages, starImages
915 
916  def addPixCoords(self, fakeCat, image):
917 
918  """Add pixel coordinates to the catalog of fakes.
919 
920  Parameters
921  ----------
922  fakeCat : `pandas.core.frame.DataFrame`
923  The catalog of fake sources to be input
924  image : `lsst.afw.image.exposure.exposure.ExposureF`
925  The image into which the fake sources should be added
926 
927  Returns
928  -------
929  fakeCat : `pandas.core.frame.DataFrame`
930  """
931  wcs = image.getWcs()
932  ras = fakeCat['ra'].values
933  decs = fakeCat['dec'].values
934  xs, ys = wcs.skyToPixelArray(ras, decs)
935  fakeCat["x"] = xs
936  fakeCat["y"] = ys
937 
938  return fakeCat
939 
940  def trimFakeCat(self, fakeCat, image):
941  """Trim the fake cat to about the size of the input image.
942 
943  `fakeCat` must be processed with addPixCoords before using this method.
944 
945  Parameters
946  ----------
947  fakeCat : `pandas.core.frame.DataFrame`
948  The catalog of fake sources to be input
949  image : `lsst.afw.image.exposure.exposure.ExposureF`
950  The image into which the fake sources should be added
951 
952  Returns
953  -------
954  fakeCat : `pandas.core.frame.DataFrame`
955  The original fakeCat trimmed to the area of the image
956  """
957 
958  bbox = Box2D(image.getBBox()).dilatedBy(self.config.trimBuffer)
959  xs = fakeCat["x"].values
960  ys = fakeCat["y"].values
961 
962  isContained = xs >= bbox.minX
963  isContained &= xs <= bbox.maxX
964  isContained &= ys >= bbox.minY
965  isContained &= ys <= bbox.maxY
966 
967  return fakeCat[isContained]
968 
969  def mkFakeGalsimGalaxies(self, fakeCat, band, photoCalib, pixelScale, psf, image):
970  """Make images of fake galaxies using GalSim.
971 
972  Parameters
973  ----------
974  band : `str`
975  pixelScale : `float`
976  psf : `lsst.meas.extensions.psfex.psfexPsf.PsfexPsf`
977  The PSF information to use to make the PSF images
978  fakeCat : `pandas.core.frame.DataFrame`
979  The catalog of fake sources to be input
980  photoCalib : `lsst.afw.image.photoCalib.PhotoCalib`
981  Photometric calibration to be used to calibrate the fake sources
982 
983  Yields
984  -------
985  galImages : `generator`
986  A generator of tuples of `lsst.afw.image.exposure.exposure.ExposureF` and
987  `lsst.geom.Point2D` of their locations.
988 
989  Notes
990  -----
991 
992  Fake galaxies are made by combining two sersic profiles, one for the bulge and one for the disk. Each
993  component has an individual sersic index (n), a, b and position angle (PA). The combined profile is
994  then convolved with the PSF at the specified x, y position on the image.
995 
996  The names of the columns in the ``fakeCat`` are configurable and are the column names from the
997  University of Washington simulations database as default. For more information see the doc strings
998  attached to the config options.
999 
1000  See mkFakeStars doc string for an explanation of calibration to instrumental flux.
1001  """
1002 
1003  self.log.info("Making %d fake galaxy images" % len(fakeCat))
1004 
1005  for (index, row) in fakeCat.iterrows():
1006  xy = geom.Point2D(row["x"], row["y"])
1007 
1008  # We put these two PSF calculations within this same try block so that we catch cases
1009  # where the object's position is outside of the image.
1010  try:
1011  correctedFlux = psf.computeApertureFlux(self.config.calibFluxRadius, xy)
1012  psfKernel = psf.computeKernelImage(xy).getArray()
1013  psfKernel /= correctedFlux
1014 
1015  except InvalidParameterError:
1016  self.log.info("Galaxy at %0.4f, %0.4f outside of image" % (row["x"], row["y"]))
1017  continue
1018 
1019  try:
1020  flux = photoCalib.magnitudeToInstFlux(row['mag'], xy)
1021  except LogicError:
1022  flux = 0
1023 
1024  # GalSim convention: HLR = sqrt(a * b) = a * sqrt(b / a)
1025  bulge_gs_HLR = row['bulge_semimajor']*np.sqrt(row['bulge_axis_ratio'])
1026  bulge = galsim.Sersic(n=row['bulge_n'], half_light_radius=bulge_gs_HLR)
1027  bulge = bulge.shear(q=row['bulge_axis_ratio'], beta=((90 - row['bulge_pa'])*galsim.degrees))
1028 
1029  disk_gs_HLR = row['disk_semimajor']*np.sqrt(row['disk_axis_ratio'])
1030  disk = galsim.Sersic(n=row['disk_n'], half_light_radius=disk_gs_HLR)
1031  disk = disk.shear(q=row['disk_axis_ratio'], beta=((90 - row['disk_pa'])*galsim.degrees))
1032 
1033  gal = bulge*row['bulge_disk_flux_ratio'] + disk
1034  gal = gal.withFlux(flux)
1035 
1036  psfIm = galsim.InterpolatedImage(galsim.Image(psfKernel), scale=pixelScale)
1037  gal = galsim.Convolve([gal, psfIm])
1038  try:
1039  galIm = gal.drawImage(scale=pixelScale, method="real_space").array
1040  except (galsim.errors.GalSimFFTSizeError, MemoryError):
1041  continue
1042 
1043  yield (afwImage.ImageF(galIm), xy)
1044 
1045  def mkFakeStars(self, fakeCat, band, photoCalib, psf, image):
1046 
1047  """Make fake stars based off the properties in the fakeCat.
1048 
1049  Parameters
1050  ----------
1051  band : `str`
1052  psf : `lsst.meas.extensions.psfex.psfexPsf.PsfexPsf`
1053  The PSF information to use to make the PSF images
1054  fakeCat : `pandas.core.frame.DataFrame`
1055  The catalog of fake sources to be input
1056  image : `lsst.afw.image.exposure.exposure.ExposureF`
1057  The image into which the fake sources should be added
1058  photoCalib : `lsst.afw.image.photoCalib.PhotoCalib`
1059  Photometric calibration to be used to calibrate the fake sources
1060 
1061  Yields
1062  -------
1063  starImages : `generator`
1064  A generator of tuples of `lsst.afw.image.ImageF` of fake stars and
1065  `lsst.geom.Point2D` of their locations.
1066 
1067  Notes
1068  -----
1069  To take a given magnitude and translate to the number of counts in the image
1070  we use photoCalib.magnitudeToInstFlux, which returns the instrumental flux for the
1071  given calibration radius used in the photometric calibration step.
1072  Thus `calibFluxRadius` should be set to this same radius so that we can normalize
1073  the PSF model to the correct instrumental flux within calibFluxRadius.
1074  """
1075 
1076  self.log.info("Making %d fake star images" % len(fakeCat))
1077 
1078  for (index, row) in fakeCat.iterrows():
1079  xy = geom.Point2D(row["x"], row["y"])
1080 
1081  # We put these two PSF calculations within this same try block so that we catch cases
1082  # where the object's position is outside of the image.
1083  try:
1084  correctedFlux = psf.computeApertureFlux(self.config.calibFluxRadius, xy)
1085  starIm = psf.computeImage(xy)
1086  starIm /= correctedFlux
1087 
1088  except InvalidParameterError:
1089  self.log.info("Star at %0.4f, %0.4f outside of image" % (row["x"], row["y"]))
1090  continue
1091 
1092  try:
1093  flux = photoCalib.magnitudeToInstFlux(row['mag'], xy)
1094  except LogicError:
1095  flux = 0
1096 
1097  starIm *= flux
1098  yield ((starIm.convertF(), xy))
1099 
1100  def cleanCat(self, fakeCat, starCheckVal):
1101  """Remove rows from the fakes catalog which have HLR = 0 for either the buldge or disk component,
1102  also remove galaxies that have Sersic index outside the galsim min and max
1103  allowed (0.3 <= n <= 6.2).
1104 
1105  Parameters
1106  ----------
1107  fakeCat : `pandas.core.frame.DataFrame`
1108  The catalog of fake sources to be input
1109  starCheckVal : `str`, `bytes` or `int`
1110  The value that is set in the sourceType column to specifiy an object is a star.
1111 
1112  Returns
1113  -------
1114  fakeCat : `pandas.core.frame.DataFrame`
1115  The input catalog of fake sources but with the bad objects removed
1116  """
1117 
1118  rowsToKeep = (((fakeCat['bulge_semimajor'] != 0.0) & (fakeCat['disk_semimajor'] != 0.0))
1119  | (fakeCat[self.config.sourceType] == starCheckVal))
1120  numRowsNotUsed = len(fakeCat) - len(np.where(rowsToKeep)[0])
1121  self.log.info("Removing %d rows with HLR = 0 for either the bulge or disk" % numRowsNotUsed)
1122  fakeCat = fakeCat[rowsToKeep]
1123 
1124  minN = galsim.Sersic._minimum_n
1125  maxN = galsim.Sersic._maximum_n
1126  rowsWithGoodSersic = (((fakeCat['bulge_n'] >= minN) & (fakeCat['bulge_n'] <= maxN)
1127  & (fakeCat['disk_n'] >= minN) & (fakeCat['disk_n'] <= maxN))
1128  | (fakeCat[self.config.sourceType] == starCheckVal))
1129  numRowsNotUsed = len(fakeCat) - len(np.where(rowsWithGoodSersic)[0])
1130  self.log.info("Removing %d rows of galaxies with nBulge or nDisk outside of %0.2f <= n <= %0.2f" %
1131  (numRowsNotUsed, minN, maxN))
1132  fakeCat = fakeCat[rowsWithGoodSersic]
1133 
1134  if self.config.doSubSelectSources:
1135  numRowsNotUsed = len(fakeCat) - len(fakeCat['select'])
1136  self.log.info("Removing %d rows which were not designated as template sources" % numRowsNotUsed)
1137  fakeCat = fakeCat[fakeCat['select']]
1138 
1139  return fakeCat
1140 
1141  def addFakeSources(self, image, fakeImages, sourceType):
1142  """Add the fake sources to the given image
1143 
1144  Parameters
1145  ----------
1146  image : `lsst.afw.image.exposure.exposure.ExposureF`
1147  The image into which the fake sources should be added
1148  fakeImages : `typing.Iterator` [`tuple` ['lsst.afw.image.ImageF`, `lsst.geom.Point2d`]]
1149  An iterator of tuples that contains (or generates) images of fake sources,
1150  and the locations they are to be inserted at.
1151  sourceType : `str`
1152  The type (star/galaxy) of fake sources input
1153 
1154  Returns
1155  -------
1156  image : `lsst.afw.image.exposure.exposure.ExposureF`
1157 
1158  Notes
1159  -----
1160  Uses the x, y information in the ``fakeCat`` to position an image of the fake interpolated onto the
1161  pixel grid of the image. Sets the ``FAKE`` mask plane for the pixels added with the fake source.
1162  """
1163 
1164  imageBBox = image.getBBox()
1165  imageMI = image.maskedImage
1166 
1167  for (fakeImage, xy) in fakeImages:
1168  X0 = xy.getX() - fakeImage.getWidth()/2 + 0.5
1169  Y0 = xy.getY() - fakeImage.getHeight()/2 + 0.5
1170  self.log.debug("Adding fake source at %d, %d" % (xy.getX(), xy.getY()))
1171  if sourceType == "galaxy":
1172  interpFakeImage = afwMath.offsetImage(fakeImage, X0, Y0, "lanczos3")
1173  else:
1174  interpFakeImage = fakeImage
1175 
1176  interpFakeImBBox = interpFakeImage.getBBox()
1177  interpFakeImBBox.clip(imageBBox)
1178 
1179  if interpFakeImBBox.getArea() > 0:
1180  imageMIView = imageMI[interpFakeImBBox]
1181  clippedFakeImage = interpFakeImage[interpFakeImBBox]
1182  clippedFakeImageMI = afwImage.MaskedImageF(clippedFakeImage)
1183  clippedFakeImageMI.mask.set(self.bitmask)
1184  imageMIView += clippedFakeImageMI
1185 
1186  return image
1187 
1188  def _getMetadataName(self):
1189  """Disable metadata writing"""
1190  return None
def run(self, skyInfo, tempExpRefList, imageScalerList, weightList, altMaskList=None, mask=None, supplementaryData=None)
def addPixCoords(self, fakeCat, image)
Definition: insertFakes.py:916
def mkFakeStars(self, fakeCat, band, photoCalib, psf, image)
def mkFakeGalsimGalaxies(self, fakeCat, band, photoCalib, pixelScale, psf, image)
Definition: insertFakes.py:969
def cleanCat(self, fakeCat, starCheckVal)
def processImagesForInsertion(self, fakeCat, wcs, psf, photoCalib, band, pixelScale)
Definition: insertFakes.py:829
def trimFakeCat(self, fakeCat, image)
Definition: insertFakes.py:940
def addFakeSources(self, image, fakeImages, sourceType)