23 Insert fake sources into calexps 
   25 from astropy.table 
import Table
 
   27 import lsst.pex.config 
as pexConfig
 
   31 from .insertFakes 
import InsertFakesTask
 
   33 from lsst.meas.base import (SingleFrameMeasurementTask, ApplyApCorrTask, CatalogCalculationTask,
 
   34                             PerTractCcdDataIdContainer)
 
   38 from lsst.pipe.base import PipelineTask, PipelineTaskConfig, CmdLineTask, PipelineTaskConnections
 
   43 __all__ = [
"ProcessCcdWithFakesConfig", 
"ProcessCcdWithFakesTask"]
 
   47                                      defaultTemplates={
"CoaddName": 
"deep"}):
 
   50         doc=
"Exposure into which fakes are to be added.",
 
   52         storageClass=
"ExposureF",
 
   53         dimensions=(
"instrument", 
"visit", 
"detector")
 
   57         doc=
"Catalog of fake sources to draw inputs from.",
 
   58         name=
"{CoaddName}Coadd_fakeSourceCat",
 
   59         storageClass=
"Parquet",
 
   60         dimensions=(
"tract", 
"skymap")
 
   64         doc=
"WCS information for the input exposure.",
 
   67         dimensions=(
"Tract", 
"SkyMap", 
"Instrument", 
"Visit", 
"Detector")
 
   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")
 
   77     icSourceCat = cT.Input(
 
   78         doc=
"Catalog of calibration sources",
 
   80         storageClass=
"sourceCatalog",
 
   81         dimensions=(
"tract", 
"skymap", 
"instrument", 
"visit", 
"detector")
 
   84     sfdSourceCat = cT.Input(
 
   85         doc=
"Catalog of calibration sources",
 
   87         storageClass=
"sourceCatalog",
 
   88         dimensions=(
"tract", 
"skymap", 
"instrument", 
"visit", 
"detector")
 
   91     outputExposure = cT.Output(
 
   92         doc=
"Exposure with fake sources added.",
 
   94         storageClass=
"ExposureF",
 
   95         dimensions=(
"instrument", 
"visit", 
"detector")
 
   98     outputCat = cT.Output(
 
   99         doc=
"Source catalog produced in calibrate task with fakes also measured.",
 
  101         storageClass=
"SourceCatalog",
 
  102         dimensions=(
"instrument", 
"visit", 
"detector"),
 
  107                                 pipelineConnections=ProcessCcdWithFakesConnections):
 
  108     """Config for inserting fake sources 
  112     The default column names are those from the UW sims database. 
  115     useUpdatedCalibs = pexConfig.Field(
 
  116         doc=
"Use updated calibs and wcs from jointcal?",
 
  121     coaddName = pexConfig.Field(
 
  122         doc=
"The name of the type of coadd used",
 
  127     calibrationFieldsToCopy = pexConfig.ListField(
 
  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.")
 
  135     srcFieldsToCopy = pexConfig.ListField(
 
  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.")
 
  143     matchRadiusPix = pexConfig.Field(
 
  146         doc=(
"Match radius for matching icSourceCat objects to sourceCat objects (pixels)"),
 
  149     insertFakes = pexConfig.ConfigurableField(target=InsertFakesTask,
 
  150                                               doc=
"Configuration for the fake sources")
 
  152     detection = pexConfig.ConfigurableField(target=SourceDetectionTask,
 
  153                                             doc=
"The detection task to use.")
 
  155     deblend = pexConfig.ConfigurableField(target=SourceDeblendTask, doc=
"The deblending task to use.")
 
  157     measurement = pexConfig.ConfigurableField(target=SingleFrameMeasurementTask,
 
  158                                               doc=
"The measurement task to use")
 
  160     applyApCorr = pexConfig.ConfigurableField(target=ApplyApCorrTask,
 
  161                                               doc=
"The apply aperture correction task to use.")
 
  163     catalogCalculation = pexConfig.ConfigurableField(target=CatalogCalculationTask,
 
  164                                                      doc=
"The catalog calculation task to use.")
 
  166     skySources = pexConfig.ConfigurableField(target=SkyObjectsTask, doc=
"Generate sky sources")
 
  169         self.detection.reEstimateBackground = 
False 
  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"]
 
  178     """Insert fake objects into calexps. 
  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. 
  184     `ProcessFakeSourcesTask` inherits six functions from insertFakesTask that make images of the fake 
  185     sources and then add them to the calexp. 
  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. 
  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. 
  195         Use the PSF information from the calexp to make a fake star using the magnitude information from the 
  198         Remove rows of the input fake catalog which have half light radius, of either the bulge or the disk, 
  201         Add the fake sources to the calexp. 
  205     The ``calexp`` with fake souces added to it is written out as the datatype ``calexp_fakes``. 
  208     _DefaultName = 
"processCcdWithFakes" 
  209     ConfigClass = ProcessCcdWithFakesConfig
 
  211     def __init__(self, schema=None, butler=None, **kwargs):
 
  212         """Initalize things! This should go above in the class docstring 
  215         super().__init__(**kwargs)
 
  218             schema = SourceTable.makeMinimalSchema()
 
  220         self.makeSubtask(
"insertFakes")
 
  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.")
 
  230     def runDataRef(self, dataRef):
 
  231         """Read in/write out the required data products and add fake sources to the calexp. 
  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: 
  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. 
  249         exposureIdInfo = dataRef.get(
"expIdInfo")
 
  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()
 
  256             fakeCat = Table.read(self.config.insertFakes.fakeType).to_pandas()
 
  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")
 
  264             wcs = calexp.getWcs()
 
  265             photoCalib = calexp.getPhotoCalib()
 
  267         icSourceCat = dataRef.get(
"icSrc", immediate=
True)
 
  268         sfdSourceCat = dataRef.get(
"src", immediate=
True)
 
  270         resultStruct = self.run(fakeCat, calexp, wcs=wcs, photoCalib=photoCalib,
 
  271                                 exposureIdInfo=exposureIdInfo, icSourceCat=icSourceCat,
 
  272                                 sfdSourceCat=sfdSourceCat)
 
  274         dataRef.put(resultStruct.outputExposure, 
"fakes_calexp")
 
  275         dataRef.put(resultStruct.outputCat, 
"fakes_src")
 
  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)
 
  284         if inputs[
"wcs"] 
is None:
 
  285             inputs[
"wcs"] = inputs[
"image"].getWcs()
 
  286         if inputs[
"photoCalib"] 
is None:
 
  287             inputs[
"photoCalib"] = inputs[
"image"].getPhotoCalib()
 
  289         outputs = self.run(**inputs)
 
  290         butlerQC.put(outputs, outputRefs)
 
  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)
 
  300     def run(self, fakeCat, exposure, wcs=None, photoCalib=None, exposureIdInfo=None, icSourceCat=None,
 
  302         """Add fake sources to a calexp and then run detection, deblending and measurement. 
  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` 
  317                     Catalog to take the information about which sources were used for calibration from. 
  318         sfdSourceCat : `lsst.afw.table.SourceCatalog` 
  320                     Catalog produced by singleFrameDriver, needed to copy some calibration flags from. 
  324         resultStruct : `lsst.pipe.base.struct.Struct` 
  325             contains : outputExposure : `lsst.afw.image.exposure.exposure.ExposureF` 
  326                        outputCat : `lsst.afw.table.source.source.SourceCatalog` 
  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 
  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. 
  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. 
  342         If exposureIdInfo is not provided then the SourceCatalog IDs will not be globally unique. 
  346             wcs = exposure.getWcs()
 
  348         if photoCalib 
is None:
 
  349             photoCalib = exposure.getPhotoCalib()
 
  351         self.insertFakes.
run(fakeCat, exposure, wcs, photoCalib)
 
  354         if exposureIdInfo 
is None:
 
  357         sourceIdFactory = IdFactory.makeSource(exposureIdInfo.expId, exposureIdInfo.unusedBits)
 
  358         table = SourceTable.make(self.schema, sourceIdFactory)
 
  359         table.setMetadata(self.algMetadata)
 
  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()
 
  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)
 
  376         resultStruct = pipeBase.Struct(outputExposure=exposure, outputCat=sourceCat)
 
  379     def copyCalibrationFields(self, calibCat, sourceCat, fieldsToCopy):
 
  380         """Match sources in calibCat and sourceCat and copy the specified fields 
  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. 
  393         newCat : `lsst.afw.table.SourceCatalog` 
  394             Catalog which includes the copied fields. 
  396         The fields copied are those specified by `fieldsToCopy` that actually exist 
  397         in the schema of `calibCat`. 
  399         This version was based on and adapted from the one in calibrateTask. 
  404         sourceSchemaMapper.addMinimalSchema(sourceCat.schema, 
True)
 
  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())
 
  417                 missingFieldNames.append(fieldName)
 
  418         if missingFieldNames:
 
  419             raise RuntimeError(f
"calibCat is missing fields {missingFieldNames} specified in " 
  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"))
 
  426             self.calibSourceKey = 
None 
  428         schema = calibSchemaMapper.getOutputSchema()
 
  430         newCat.reserve(len(sourceCat))
 
  431         newCat.extend(sourceCat, sourceSchemaMapper)
 
  434         for k, v 
in sourceCat.schema.getAliasMap().
items():
 
  435             newCat.schema.getAliasMap().
set(k, v)
 
  437         select = newCat[
"deblend_nChild"] == 0
 
  438         matches = 
afwTable.matchXy(newCat[select], calibCat, self.config.matchRadiusPix)
 
  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,
 
  448         self.log.
info(
"Copying flags from calibCat to sourceCat for %s sources", numMatches)
 
  452         for src, calibSrc, d 
in matches:
 
  453             if self.calibSourceKey:
 
  454                 src.setFlag(self.calibSourceKey, 
True)
 
  459             calibSrcFootprint = calibSrc.getFootprint()
 
  461                 calibSrc.setFootprint(src.getFootprint())
 
  462                 src.assign(calibSrc, calibSchemaMapper)
 
  464                 calibSrc.setFootprint(calibSrcFootprint)