LSSTApplications  20.0.0
LSSTDataManagementBasePackage
processCcdWithFakes.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 fake sources into calexps
24 """
25 from astropy.table import Table
26 
27 import lsst.pex.config as pexConfig
28 import lsst.pipe.base as pipeBase
29 import lsst.daf.base as dafBase
30 
31 from .insertFakes import InsertFakesTask
32 from lsst.meas.algorithms import SourceDetectionTask, SkyObjectsTask
33 from lsst.meas.base import (SingleFrameMeasurementTask, ApplyApCorrTask, CatalogCalculationTask,
34  PerTractCcdDataIdContainer)
35 from lsst.meas.deblender import SourceDeblendTask
36 from lsst.afw.table import SourceTable, IdFactory
37 from lsst.obs.base import ExposureIdInfo
38 from lsst.pipe.base import PipelineTask, PipelineTaskConfig, CmdLineTask, PipelineTaskConnections
40 import lsst.afw.table as afwTable
41 
42 
43 __all__ = ["ProcessCcdWithFakesConfig", "ProcessCcdWithFakesTask"]
44 
45 
46 class ProcessCcdWithFakesConnections(PipelineTaskConnections, dimensions=("instrument", "visit", "detector"),
47  defaultTemplates={"CoaddName": "deep"}):
48 
49  exposure = cT.Input(
50  doc="Exposure into which fakes are to be added.",
51  name="calexp",
52  storageClass="ExposureF",
53  dimensions=("instrument", "visit", "detector")
54  )
55 
56  fakeCat = cT.Input(
57  doc="Catalog of fake sources to draw inputs from.",
58  name="{CoaddName}Coadd_fakeSourceCat",
59  storageClass="Parquet",
60  dimensions=("tract", "skymap")
61  )
62 
63  wcs = cT.Input(
64  doc="WCS information for the input exposure.",
65  name="jointcal_wcs",
66  storageClass="Wcs",
67  dimensions=("Tract", "SkyMap", "Instrument", "Visit", "Detector")
68  )
69 
70  photoCalib = cT.Input(
71  doc="Calib information for the input exposure.",
72  name="jointcal_photoCalib",
73  storageClass="PhotoCalib",
74  dimensions=("Tract", "SkyMap", "Instrument", "Visit", "Detector")
75  )
76 
77  icSourceCat = cT.Input(
78  doc="Catalog of calibration sources",
79  name="icSrc",
80  storageClass="sourceCatalog",
81  dimensions=("tract", "skymap", "instrument", "visit", "detector")
82  )
83 
84  sfdSourceCat = cT.Input(
85  doc="Catalog of calibration sources",
86  name="src",
87  storageClass="sourceCatalog",
88  dimensions=("tract", "skymap", "instrument", "visit", "detector")
89  )
90 
91  outputExposure = cT.Output(
92  doc="Exposure with fake sources added.",
93  name="fakes_calexp",
94  storageClass="ExposureF",
95  dimensions=("instrument", "visit", "detector")
96  )
97 
98  outputCat = cT.Output(
99  doc="Source catalog produced in calibrate task with fakes also measured.",
100  name="fakes_src",
101  storageClass="SourceCatalog",
102  dimensions=("instrument", "visit", "detector"),
103  )
104 
105 
106 class ProcessCcdWithFakesConfig(PipelineTaskConfig,
107  pipelineConnections=ProcessCcdWithFakesConnections):
108  """Config for inserting fake sources
109 
110  Notes
111  -----
112  The default column names are those from the UW sims database.
113  """
114 
115  useUpdatedCalibs = pexConfig.Field(
116  doc="Use updated calibs and wcs from jointcal?",
117  dtype=bool,
118  default=False,
119  )
120 
121  coaddName = pexConfig.Field(
122  doc="The name of the type of coadd used",
123  dtype=str,
124  default="deep",
125  )
126 
127  calibrationFieldsToCopy = pexConfig.ListField(
128  dtype=str,
129  default=("calib_psf_candidate", "calib_psf_used", "calib_psf_reserved"),
130  doc=("Fields to copy from the icSource catalog to the output catalog "
131  "for matching sources Any missing fields will trigger a "
132  "RuntimeError exception.")
133  )
134 
135  srcFieldsToCopy = pexConfig.ListField(
136  dtype=str,
137  default=("calib_photometry_reserved", "calib_photometry_used", "calib_astrometry_used"),
138  doc=("Fields to copy from the `src` catalog to the output catalog "
139  "for matching sources Any missing fields will trigger a "
140  "RuntimeError exception.")
141  )
142 
143  matchRadiusPix = pexConfig.Field(
144  dtype=float,
145  default=3,
146  doc=("Match radius for matching icSourceCat objects to sourceCat objects (pixels)"),
147  )
148 
149  insertFakes = pexConfig.ConfigurableField(target=InsertFakesTask,
150  doc="Configuration for the fake sources")
151 
152  detection = pexConfig.ConfigurableField(target=SourceDetectionTask,
153  doc="The detection task to use.")
154 
155  deblend = pexConfig.ConfigurableField(target=SourceDeblendTask, doc="The deblending task to use.")
156 
157  measurement = pexConfig.ConfigurableField(target=SingleFrameMeasurementTask,
158  doc="The measurement task to use")
159 
160  applyApCorr = pexConfig.ConfigurableField(target=ApplyApCorrTask,
161  doc="The apply aperture correction task to use.")
162 
163  catalogCalculation = pexConfig.ConfigurableField(target=CatalogCalculationTask,
164  doc="The catalog calculation task to use.")
165 
166  skySources = pexConfig.ConfigurableField(target=SkyObjectsTask, doc="Generate sky sources")
167 
168  def setDefaults(self):
169  self.detection.reEstimateBackground = False
170  super().setDefaults()
171  self.measurement.plugins["base_PixelFlags"].masksFpAnywhere.append("FAKE")
172  self.measurement.plugins["base_PixelFlags"].masksFpCenter.append("FAKE")
173  self.deblend.maxFootprintSize = 2000
174  self.measurement.plugins.names |= ["base_LocalPhotoCalib", "base_LocalWcs"]
175 
176 
177 class ProcessCcdWithFakesTask(PipelineTask, CmdLineTask):
178  """Insert fake objects into calexps.
179 
180  Add fake stars and galaxies to the given calexp, specified in the dataRef. Galaxy parameters are read in
181  from the specified file and then modelled using galsim. Re-runs characterize image and calibrate image to
182  give a new background estimation and measurement of the calexp.
183 
184  `ProcessFakeSourcesTask` inherits six functions from insertFakesTask that make images of the fake
185  sources and then add them to the calexp.
186 
187  `addPixCoords`
188  Use the WCS information to add the pixel coordinates of each source
189  Adds an ``x`` and ``y`` column to the catalog of fake sources.
190  `trimFakeCat`
191  Trim the fake cat to about the size of the input image.
192  `mkFakeGalsimGalaxies`
193  Use Galsim to make fake double sersic galaxies for each set of galaxy parameters in the input file.
194  `mkFakeStars`
195  Use the PSF information from the calexp to make a fake star using the magnitude information from the
196  input file.
197  `cleanCat`
198  Remove rows of the input fake catalog which have half light radius, of either the bulge or the disk,
199  that are 0.
200  `addFakeSources`
201  Add the fake sources to the calexp.
202 
203  Notes
204  -----
205  The ``calexp`` with fake souces added to it is written out as the datatype ``calexp_fakes``.
206  """
207 
208  _DefaultName = "processCcdWithFakes"
209  ConfigClass = ProcessCcdWithFakesConfig
210 
211  def __init__(self, schema=None, butler=None, **kwargs):
212  """Initalize things! This should go above in the class docstring
213  """
214 
215  super().__init__(**kwargs)
216 
217  if schema is None:
218  schema = SourceTable.makeMinimalSchema()
219  self.schema = schema
220  self.makeSubtask("insertFakes")
221  self.algMetadata = dafBase.PropertyList()
222  self.makeSubtask("detection", schema=self.schema)
223  self.makeSubtask("deblend", schema=self.schema)
224  self.makeSubtask("measurement", schema=self.schema, algMetadata=self.algMetadata)
225  self.makeSubtask("applyApCorr", schema=self.schema)
226  self.makeSubtask("catalogCalculation", schema=self.schema)
227  self.makeSubtask("skySources")
228  self.skySourceKey = self.schema.addField("sky_source", type="Flag", doc="Sky objects.")
229 
230  def runDataRef(self, dataRef):
231  """Read in/write out the required data products and add fake sources to the calexp.
232 
233  Parameters
234  ----------
235  dataRef : `lsst.daf.persistence.butlerSubset.ButlerDataRef`
236  Data reference defining the ccd to have fakes added to it.
237  Used to access the following data products:
238  calexp
239  jointcal_wcs
240  jointcal_photoCalib
241 
242  Notes
243  -----
244  Uses the calibration and WCS information attached to the calexp for the posistioning and calibration
245  of the sources unless the config option config.useUpdatedCalibs is set then it uses the
246  meas_mosaic/jointCal outputs. The config defualts for the column names in the catalog of fakes are
247  taken from the University of Washington simulations database. Operates on one ccd at a time.
248  """
249  exposureIdInfo = dataRef.get("expIdInfo")
250 
251  if self.config.insertFakes.fakeType == "snapshot":
252  fakeCat = dataRef.get("fakeSourceCat").toDataFrame()
253  elif self.config.insertFakes.fakeType == "static":
254  fakeCat = dataRef.get("deepCoadd_fakeSourceCat").toDataFrame()
255  else:
256  fakeCat = Table.read(self.config.insertFakes.fakeType).to_pandas()
257 
258  calexp = dataRef.get("calexp")
259  if self.config.useUpdatedCalibs:
260  self.log.info("Using updated calibs from meas_mosaic/jointCal")
261  wcs = dataRef.get("jointcal_wcs")
262  photoCalib = dataRef.get("jointcal_photoCalib")
263  else:
264  wcs = calexp.getWcs()
265  photoCalib = calexp.getPhotoCalib()
266 
267  icSourceCat = dataRef.get("icSrc", immediate=True)
268  sfdSourceCat = dataRef.get("src", immediate=True)
269 
270  resultStruct = self.run(fakeCat, calexp, wcs=wcs, photoCalib=photoCalib,
271  exposureIdInfo=exposureIdInfo, icSourceCat=icSourceCat,
272  sfdSourceCat=sfdSourceCat)
273 
274  dataRef.put(resultStruct.outputExposure, "fakes_calexp")
275  dataRef.put(resultStruct.outputCat, "fakes_src")
276  return resultStruct
277 
278  def runQuantum(self, butlerQC, inputRefs, outputRefs):
279  inputs = butlerQC.get(inputRefs)
280  if 'exposureIdInfo' not in inputs.keys():
281  expId, expBits = butlerQC.quantum.dataId.pack("visit_detector", returnMaxBits=True)
282  inputs['exposureIdInfo'] = ExposureIdInfo(expId, expBits)
283 
284  if inputs["wcs"] is None:
285  inputs["wcs"] = inputs["image"].getWcs()
286  if inputs["photoCalib"] is None:
287  inputs["photoCalib"] = inputs["image"].getPhotoCalib()
288 
289  outputs = self.run(**inputs)
290  butlerQC.put(outputs, outputRefs)
291 
292  @classmethod
293  def _makeArgumentParser(cls):
294  parser = pipeBase.ArgumentParser(name=cls._DefaultName)
295  parser.add_id_argument("--id", "fakes_calexp", help="data ID with raw CCD keys [+ tract optionally], "
296  "e.g. --id visit=12345 ccd=1,2 [tract=0]",
297  ContainerClass=PerTractCcdDataIdContainer)
298  return parser
299 
300  def run(self, fakeCat, exposure, wcs=None, photoCalib=None, exposureIdInfo=None, icSourceCat=None,
301  sfdSourceCat=None):
302  """Add fake sources to a calexp and then run detection, deblending and measurement.
303 
304  Parameters
305  ----------
306  fakeCat : `pandas.core.frame.DataFrame`
307  The catalog of fake sources to add to the exposure
308  exposure : `lsst.afw.image.exposure.exposure.ExposureF`
309  The exposure to add the fake sources to
310  wcs : `lsst.afw.geom.SkyWcs`
311  WCS to use to add fake sources
312  photoCalib : `lsst.afw.image.photoCalib.PhotoCalib`
313  Photometric calibration to be used to calibrate the fake sources
314  exposureIdInfo : `lsst.obs.base.ExposureIdInfo`
315  icSourceCat : `lsst.afw.table.SourceCatalog`
316  Default : None
317  Catalog to take the information about which sources were used for calibration from.
318  sfdSourceCat : `lsst.afw.table.SourceCatalog`
319  Default : None
320  Catalog produced by singleFrameDriver, needed to copy some calibration flags from.
321 
322  Returns
323  -------
324  resultStruct : `lsst.pipe.base.struct.Struct`
325  contains : outputExposure : `lsst.afw.image.exposure.exposure.ExposureF`
326  outputCat : `lsst.afw.table.source.source.SourceCatalog`
327 
328  Notes
329  -----
330  Adds pixel coordinates for each source to the fakeCat and removes objects with bulge or disk half
331  light radius = 0 (if ``config.cleanCat = True``). These columns are called ``x`` and ``y`` and are in
332  pixels.
333 
334  Adds the ``Fake`` mask plane to the exposure which is then set by `addFakeSources` to mark where fake
335  sources have been added. Uses the information in the ``fakeCat`` to make fake galaxies (using galsim)
336  and fake stars, using the PSF models from the PSF information for the calexp. These are then added to
337  the calexp and the calexp with fakes included returned.
338 
339  The galsim galaxies are made using a double sersic profile, one for the bulge and one for the disk,
340  this is then convolved with the PSF at that point.
341 
342  If exposureIdInfo is not provided then the SourceCatalog IDs will not be globally unique.
343  """
344 
345  if wcs is None:
346  wcs = exposure.getWcs()
347 
348  if photoCalib is None:
349  photoCalib = exposure.getPhotoCalib()
350 
351  self.insertFakes.run(fakeCat, exposure, wcs, photoCalib)
352 
353  # detect, deblend and measure sources
354  if exposureIdInfo is None:
355  exposureIdInfo = ExposureIdInfo()
356 
357  sourceIdFactory = IdFactory.makeSource(exposureIdInfo.expId, exposureIdInfo.unusedBits)
358  table = SourceTable.make(self.schema, sourceIdFactory)
359  table.setMetadata(self.algMetadata)
360 
361  detRes = self.detection.run(table=table, exposure=exposure, doSmooth=True)
362  sourceCat = detRes.sources
363  skySourceFootprints = self.skySources.run(mask=exposure.mask, seed=exposureIdInfo.expId)
364  if skySourceFootprints:
365  for foot in skySourceFootprints:
366  s = sourceCat.addNew()
367  s.setFootprint(foot)
368  s.set(self.skySourceKey, True)
369  self.deblend.run(exposure=exposure, sources=sourceCat)
370  self.measurement.run(measCat=sourceCat, exposure=exposure, exposureId=exposureIdInfo.expId)
371  self.applyApCorr.run(catalog=sourceCat, apCorrMap=exposure.getInfo().getApCorrMap())
372  self.catalogCalculation.run(sourceCat)
373  sourceCat = self.copyCalibrationFields(icSourceCat, sourceCat, self.config.calibrationFieldsToCopy)
374  sourceCat = self.copyCalibrationFields(sfdSourceCat, sourceCat, self.config.srcFieldsToCopy)
375 
376  resultStruct = pipeBase.Struct(outputExposure=exposure, outputCat=sourceCat)
377  return resultStruct
378 
379  def copyCalibrationFields(self, calibCat, sourceCat, fieldsToCopy):
380  """Match sources in calibCat and sourceCat and copy the specified fields
381 
382  Parameters
383  ----------
384  calibCat : `lsst.afw.table.SourceCatalog`
385  Catalog from which to copy fields.
386  sourceCat : `lsst.afw.table.SourceCatalog`
387  Catalog to which to copy fields.
388  fieldsToCopy : `lsst.pex.config.listField.List`
389  Fields to copy from calibCat to SoourceCat.
390 
391  Returns
392  -------
393  newCat : `lsst.afw.table.SourceCatalog`
394  Catalog which includes the copied fields.
395 
396  The fields copied are those specified by `fieldsToCopy` that actually exist
397  in the schema of `calibCat`.
398 
399  This version was based on and adapted from the one in calibrateTask.
400  """
401 
402  # Make a new SourceCatalog with the data from sourceCat so that we can add the new columns to it
403  sourceSchemaMapper = afwTable.SchemaMapper(sourceCat.schema)
404  sourceSchemaMapper.addMinimalSchema(sourceCat.schema, True)
405 
406  calibSchemaMapper = afwTable.SchemaMapper(calibCat.schema, sourceCat.schema)
407 
408  # Add the desired columns from the option fieldsToCopy
409  missingFieldNames = []
410  for fieldName in fieldsToCopy:
411  if fieldName in calibCat.schema:
412  schemaItem = calibCat.schema.find(fieldName)
413  calibSchemaMapper.editOutputSchema().addField(schemaItem.getField())
414  schema = calibSchemaMapper.editOutputSchema()
415  calibSchemaMapper.addMapping(schemaItem.getKey(), schema.find(fieldName).getField())
416  else:
417  missingFieldNames.append(fieldName)
418  if missingFieldNames:
419  raise RuntimeError(f"calibCat is missing fields {missingFieldNames} specified in "
420  "fieldsToCopy")
421 
422  if "calib_detected" not in calibSchemaMapper.getOutputSchema():
423  self.calibSourceKey = calibSchemaMapper.addOutputField(afwTable.Field["Flag"]("calib_detected",
424  "Source was detected as an icSource"))
425  else:
426  self.calibSourceKey = None
427 
428  schema = calibSchemaMapper.getOutputSchema()
429  newCat = afwTable.SourceCatalog(schema)
430  newCat.reserve(len(sourceCat))
431  newCat.extend(sourceCat, sourceSchemaMapper)
432 
433  # Set the aliases so it doesn't complain.
434  for k, v in sourceCat.schema.getAliasMap().items():
435  newCat.schema.getAliasMap().set(k, v)
436 
437  select = newCat["deblend_nChild"] == 0
438  matches = afwTable.matchXy(newCat[select], calibCat, self.config.matchRadiusPix)
439  # Check that no sourceCat sources are listed twice (we already know
440  # that each match has a unique calibCat source ID, due to using
441  # that ID as the key in bestMatches)
442  numMatches = len(matches)
443  numUniqueSources = len(set(m[1].getId() for m in matches))
444  if numUniqueSources != numMatches:
445  self.log.warn("%d calibCat sources matched only %d sourceCat sources", numMatches,
446  numUniqueSources)
447 
448  self.log.info("Copying flags from calibCat to sourceCat for %s sources", numMatches)
449 
450  # For each match: set the calibSourceKey flag and copy the desired
451  # fields
452  for src, calibSrc, d in matches:
453  if self.calibSourceKey:
454  src.setFlag(self.calibSourceKey, True)
455  # src.assign copies the footprint from calibSrc, which we don't want
456  # (DM-407)
457  # so set calibSrc's footprint to src's footprint before src.assign,
458  # then restore it
459  calibSrcFootprint = calibSrc.getFootprint()
460  try:
461  calibSrc.setFootprint(src.getFootprint())
462  src.assign(calibSrc, calibSchemaMapper)
463  finally:
464  calibSrc.setFootprint(calibSrcFootprint)
465 
466  return newCat
lsst::afw::table::matchXy
SourceMatchVector matchXy(SourceCatalog const &cat, double radius, bool symmetric)
Compute all tuples (s1,s2,d) where s1 != s2, s1 and s2 both belong to cat, and d, the distance betwee...
Definition: Match.cc:383
lsst::log.log.logContinued.warn
def warn(fmt, *args)
Definition: logContinued.py:202
lsst::log.log.logContinued.info
def info(fmt, *args)
Definition: logContinued.py:198
lsst::daf::base::PropertyList
Class for storing ordered metadata with comments.
Definition: PropertyList.h:68
lsst.pipe.tasks.assembleCoadd.run
def run(self, skyInfo, tempExpRefList, imageScalerList, weightList, altMaskList=None, mask=None, supplementaryData=None)
Definition: assembleCoadd.py:712
lsst.pipe.base.pipelineTask.PipelineTask
Definition: pipelineTask.py:32
lsst::meas::base
Definition: Algorithm.h:37
lsst::afw::table._source.SourceCatalog
Definition: _source.py:33
pex.config.wrap.setDefaults
setDefaults
Definition: wrap.py:293
lsst::afw::table::SchemaMapper
A mapping between the keys of two Schemas, used to copy data between them.
Definition: SchemaMapper.h:21
lsst.pipe.base.connections.PipelineTaskConnections
Definition: connections.py:254
lsst::afw::table
Definition: table.dox:3
dimensions
afw::table::PointKey< int > dimensions
Definition: GaussianPsf.cc:49
lsst::meas::deblender
Definition: BaselineUtils.h:17
lsst.pipe.tasks.processCcdWithFakes.ProcessCcdWithFakesConnections
Definition: processCcdWithFakes.py:47
lsst.pipe.base.config.PipelineTaskConfig
Definition: config.py:113
lsst.obs.base.exposureIdInfo.ExposureIdInfo
Definition: exposureIdInfo.py:25
lsst::daf::base
Definition: Utils.h:47
items
std::vector< SchemaItem< Flag > > * items
Definition: BaseColumnView.cc:142
lsst::afw::table::Field
A description of a field in a table.
Definition: Field.h:24
lsst.pipe.base
Definition: __init__.py:1
lsst::meas::algorithms
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations.
Definition: CoaddBoundedField.h:34
set
daf::base::PropertySet * set
Definition: fits.cc:912
lsst.pipe.base.connectionTypes
Definition: connectionTypes.py:1
lsst.pipe.base.connections.PipelineTaskConnections.__init__
def __init__(self, *'PipelineTaskConfig' config=None)
Definition: connections.py:358
lsst.obs.base
Definition: __init__.py:1
lsst.pipe.base.cmdLineTask.CmdLineTask
Definition: cmdLineTask.py:492