24 import lsst.pex.config
 
   36 from .forcedPhotImage 
import ForcedPhotImageTask, ForcedPhotImageConfig
 
   39     from lsst.meas.mosaic 
import applyMosaicResults
 
   41     applyMosaicResults = 
None 
   43 __all__ = (
"PerTractCcdDataIdContainer", 
"ForcedPhotCcdConfig", 
"ForcedPhotCcdTask", 
"imageOverlapsTract")
 
   47     """A data ID container which combines raw data IDs with a tract. 
   51     Required because we need to add "tract" to the raw data ID keys (defined as 
   52     whatever we use for ``src``) when no tract is provided (so that the user is 
   53     not required to know which tracts are spanned by the raw data ID). 
   55     This subclass of `~lsst.pipe.base.DataIdContainer` assumes that a calexp is 
   56     being measured using the detection information, a set of reference 
   57     catalogs, from the set of coadds which intersect with the calexp.  It needs 
   58     the calexp id (e.g.  visit, raft, sensor), but is also uses the tract to 
   59     decide what set of coadds to use.  The references from the tract whose 
   60     patches intersect with the calexp are used. 
   64         """Make self.refList from self.idList 
   67             raise RuntimeError(
"Must call setDatasetType first")
 
   68         log = Log.getLogger(
"meas.base.forcedPhotCcd.PerTractCcdDataIdContainer")
 
   70         visitTract = collections.defaultdict(set)   
 
   71         visitRefs = collections.defaultdict(list)   
 
   73             if "tract" not in dataId:
 
   75                 log.info(
"Reading WCS for components of dataId=%s to determine tracts", dict(dataId))
 
   77                     skymap = namespace.butler.get(namespace.config.coaddName + 
"Coadd_skyMap")
 
   79                 for ref 
in namespace.butler.subset(
"calexp", dataId=dataId):
 
   80                     if not ref.datasetExists(
"calexp"):
 
   83                     visit = ref.dataId[
"visit"]
 
   84                     visitRefs[visit].
append(ref)
 
   86                     md = ref.get(
"calexp_md", immediate=
True)
 
   91                     tract = skymap.findTract(wcs.pixelToSky(box.getCenter()))
 
   93                         visitTract[visit].add(tract.getId())
 
   95                 self.
refList.extend(ref 
for ref 
in namespace.butler.subset(self.
datasetType, dataId=dataId))
 
   98         for visit, tractSet 
in visitTract.items():
 
   99             for ref 
in visitRefs[visit]:
 
  100                 for tract 
in tractSet:
 
  102                                                                  dataId=ref.dataId, tract=tract))
 
  104             tractCounter = collections.Counter()
 
  105             for tractSet 
in visitTract.values():
 
  106                 tractCounter.update(tractSet)
 
  107             log.info(
"Number of visits for each tract: %s", dict(tractCounter))
 
  111     """Return whether the given bounding box overlaps the tract given a WCS. 
  115     tract : `lsst.skymap.TractInfo` 
  116         TractInfo specifying a tract. 
  117     imageWcs : `lsst.afw.geom.SkyWcs` 
  118         World coordinate system for the image. 
  119     imageBox : `lsst.geom.Box2I` 
  120         Bounding box for the image. 
  125         `True` if the bounding box overlaps the tract; `False` otherwise. 
  127     tractPoly = tract.getOuterSkyPolygon()
 
  131         imageSkyCorners = imageWcs.pixelToSky(imagePixelCorners)
 
  132     except lsst.pex.exceptions.LsstCppException 
as e:
 
  134         if (
not isinstance(e.message, lsst.pex.exceptions.DomainErrorException)
 
  135                 and not isinstance(e.message, lsst.pex.exceptions.RuntimeErrorException)):
 
  140     return tractPoly.intersects(imagePoly)  
 
  144                                dimensions=(
"instrument", 
"visit", 
"detector", 
"skymap", 
"tract"),
 
  145                                defaultTemplates={
"inputCoaddName": 
"deep",
 
  146                                                  "inputName": 
"calexp"}):
 
  147     inputSchema = cT.InitInput(
 
  148         doc=
"Schema for the input measurement catalogs.",
 
  149         name=
"{inputCoaddName}Coadd_ref_schema",
 
  150         storageClass=
"SourceCatalog",
 
  152     outputSchema = cT.InitOutput(
 
  153         doc=
"Schema for the output forced measurement catalogs.",
 
  154         name=
"forced_src_schema",
 
  155         storageClass=
"SourceCatalog",
 
  158         doc=
"Input exposure to perform photometry on.",
 
  160         storageClass=
"ExposureF",
 
  161         dimensions=[
"instrument", 
"visit", 
"detector"],
 
  164         doc=
"Catalog of shapes and positions at which to force photometry.",
 
  165         name=
"{inputCoaddName}Coadd_ref",
 
  166         storageClass=
"SourceCatalog",
 
  167         dimensions=[
"skymap", 
"tract", 
"patch"],
 
  170         doc=
"Reference world coordinate system.",
 
  171         name=
"{inputCoaddName}Coadd.wcs",
 
  173         dimensions=[
"abstract_filter", 
"skymap", 
"tract", 
"patch"],
 
  176         doc=
"Output forced photometry catalog.",
 
  178         storageClass=
"SourceCatalog",
 
  179         dimensions=[
"instrument", 
"visit", 
"detector", 
"skymap", 
"tract"],
 
  183 class ForcedPhotCcdConfig(ForcedPhotImageConfig, pipelineConnections=ForcedPhotCcdConnections):
 
  184     doApplyUberCal = lsst.pex.config.Field(
 
  186         doc=
"Apply meas_mosaic ubercal results to input calexps?",
 
  188         deprecated=
"Deprecated by DM-23352; use doApplyExternalPhotoCalib and doApplyExternalSkyWcs instead",
 
  190     doApplyExternalPhotoCalib = lsst.pex.config.Field(
 
  193         doc=(
"Whether to apply external photometric calibration via an " 
  194              "`lsst.afw.image.PhotoCalib` object. Uses the " 
  195              "``externalPhotoCalibName`` field to determine which calibration " 
  198     doApplyExternalSkyWcs = lsst.pex.config.Field(
 
  201         doc=(
"Whether to apply external astrometric calibration via an " 
  202              "`lsst.afw.geom.SkyWcs` object. Uses ``externalSkyWcsName`` " 
  203              "field to determine which calibration to load."),
 
  205     doApplySkyCorr = lsst.pex.config.Field(
 
  208         doc=
"Apply sky correction?",
 
  210     includePhotoCalibVar = lsst.pex.config.Field(
 
  213         doc=
"Add photometric calibration variance to warp variance plane?",
 
  215     externalPhotoCalibName = lsst.pex.config.ChoiceField(
 
  217         doc=(
"Type of external PhotoCalib if ``doApplyExternalPhotoCalib`` is True. " 
  218              "Unused for Gen3 middleware."),
 
  221             "jointcal": 
"Use jointcal_photoCalib",
 
  222             "fgcm": 
"Use fgcm_photoCalib",
 
  223             "fgcm_tract": 
"Use fgcm_tract_photoCalib" 
  226     externalSkyWcsName = lsst.pex.config.ChoiceField(
 
  228         doc=
"Type of external SkyWcs if ``doApplyExternalSkyWcs`` is True. Unused for Gen3 middleware.",
 
  231             "jointcal": 
"Use jointcal_wcs" 
  236 class ForcedPhotCcdTask(ForcedPhotImageTask):
 
  237     """A command-line driver for performing forced measurement on CCD images. 
  241     This task is a subclass of 
  242     :lsst-task:`lsst.meas.base.forcedPhotImage.ForcedPhotImageTask` which is 
  243     specifically for doing forced measurement on a single CCD exposure, using 
  244     as a reference catalog the detections which were made on overlapping 
  247     The `run` method (inherited from `ForcedPhotImageTask`) takes a 
  248     `~lsst.daf.persistence.ButlerDataRef` argument that corresponds to a single 
  249     CCD. This should contain the data ID keys that correspond to the 
  250     ``forced_src`` dataset (the output dataset for this task), which are 
  251     typically all those used to specify the ``calexp`` dataset (``visit``, 
  252     ``raft``, ``sensor`` for LSST data) as well as a coadd tract. The tract is 
  253     used to look up the appropriate coadd measurement catalogs to use as 
  254     references (e.g. ``deepCoadd_src``; see 
  255     :lsst-task:`lsst.meas.base.references.CoaddSrcReferencesTask` for more 
  256     information). While the tract must be given as part of the dataRef, the 
  257     patches are determined automatically from the bounding box and WCS of the 
  258     calexp to be measured, and the filter used to fetch references is set via 
  259     the ``filter`` option in the configuration of 
  260     :lsst-task:`lsst.meas.base.references.BaseReferencesTask`). 
  262     In addition to the `run` method, `ForcedPhotCcdTask` overrides several 
  263     methods of `ForcedPhotImageTask` to specialize it for single-CCD 
  264     processing, including `~ForcedPhotImageTask.makeIdFactory`, 
  265     `~ForcedPhotImageTask.fetchReferences`, and 
  266     `~ForcedPhotImageTask.getExposure`. None of these should be called 
  267     directly by the user, though it may be useful to override them further in 
  271     ConfigClass = ForcedPhotCcdConfig
 
  273     _DefaultName = 
"forcedPhotCcd" 
  276     def runQuantum(self, butlerQC, inputRefs, outputRefs):
 
  277         inputs = butlerQC.get(inputRefs)
 
  278         inputs[
'refCat'] = self.filterReferences(inputs[
'exposure'], inputs[
'refCat'], inputs[
'refWcs'])
 
  279         inputs[
'measCat'] = self.generateMeasCat(inputRefs.exposure.dataId,
 
  281                                                  inputs[
'refCat'], inputs[
'refWcs'],
 
  284         outputs = self.run(**inputs)
 
  285         butlerQC.put(outputs, outputRefs)
 
  287     def filterReferences(self, exposure, refCat, refWcs):
 
  288         """Filter reference catalog so that all sources are within the 
  289         boundaries of the exposure. 
  293         exposure : `lsst.afw.image.exposure.Exposure` 
  294             Exposure to generate the catalog for. 
  295         refCat : `lsst.afw.table.SourceCatalog` 
  296             Catalog of shapes and positions at which to force photometry. 
  297         refWcs : `lsst.afw.image.SkyWcs` 
  298             Reference world coordinate system. 
  302         refSources : `lsst.afw.table.SourceCatalog` 
  303             Filtered catalog of forced sources to measure. 
  307         Filtering the reference catalog is currently handled by Gen2 
  308         specific methods.  To function for Gen3, this method copies 
  309         code segments to do the filtering and transformation.  The 
  310         majority of this code is based on the methods of 
  311         lsst.meas.algorithms.loadReferenceObjects.ReferenceObjectLoader 
  317         expWcs = exposure.getWcs()
 
  318         expRegion = exposure.getBBox(lsst.afw.image.PARENT)
 
  320         expBoxCorners = expBBox.getCorners()
 
  321         expSkyCorners = [expWcs.pixelToSky(corner).getVector() 
for 
  322                          corner 
in expBoxCorners]
 
  327         sources = 
type(refCat)(refCat.table)
 
  328         for record 
in refCat:
 
  329             if expPolygon.contains(record.getCoord().getVector()):
 
  330                 sources.append(record)
 
  331         refCatIdDict = {ref.getId(): ref.getParent() 
for ref 
in sources}
 
  336         refSources = 
type(refCat)(refCat.table)
 
  337         for record 
in refCat:
 
  338             if expPolygon.contains(record.getCoord().getVector()):
 
  339                 recordId = record.getId()
 
  342                     if topId 
in refCatIdDict:
 
  343                         topId = refCatIdDict[topId]
 
  347                     refSources.append(record)
 
  351         for refRecord 
in refSources:
 
  352             refRecord.setFootprint(refRecord.getFootprint().
transform(refWcs,
 
  357     def makeIdFactory(self, dataRef):
 
  358         """Create an object that generates globally unique source IDs. 
  360         Source IDs are created based on a per-CCD ID and the ID of the CCD 
  365         dataRef : `lsst.daf.persistence.ButlerDataRef` 
  366             Butler data reference. The ``ccdExposureId_bits`` and 
  367             ``ccdExposureId`` datasets are accessed. The data ID must have the 
  368             keys that correspond to ``ccdExposureId``, which are generally the 
  369             same as those that correspond to ``calexp`` (``visit``, ``raft``, 
  370             ``sensor`` for LSST data). 
  372         expBits = dataRef.get(
"ccdExposureId_bits")
 
  373         expId = int(dataRef.get(
"ccdExposureId"))
 
  376     def getExposureId(self, dataRef):
 
  377         return int(dataRef.get(
"ccdExposureId", immediate=
True))
 
  379     def fetchReferences(self, dataRef, exposure):
 
  380         """Get sources that overlap the exposure. 
  384         dataRef : `lsst.daf.persistence.ButlerDataRef` 
  385             Butler data reference corresponding to the image to be measured; 
  386             should have ``tract``, ``patch``, and ``filter`` keys. 
  387         exposure : `lsst.afw.image.Exposure` 
  388             The image to be measured (used only to obtain a WCS and bounding 
  393         referencs : `lsst.afw.table.SourceCatalog` 
  394             Catalog of sources that overlap the exposure 
  398         The returned catalog is sorted by ID and guarantees that all included 
  399         children have their parent included and that all Footprints are valid. 
  401         All work is delegated to the references subtask; see 
  402         :lsst-task:`lsst.meas.base.references.CoaddSrcReferencesTask` 
  403         for information about the default behavior. 
  407         unfiltered = self.references.fetchInBox(dataRef, exposure.getBBox(), exposure.getWcs())
 
  408         for record 
in unfiltered:
 
  409             if record.getFootprint() 
is None or record.getFootprint().getArea() == 0:
 
  410                 if record.getParent() != 0:
 
  411                     self.log.
warn(
"Skipping reference %s (child of %s) with bad Footprint",
 
  412                                   record.getId(), record.getParent())
 
  414                     self.log.
warn(
"Skipping reference parent %s with bad Footprint", record.getId())
 
  415                     badParents.add(record.getId())
 
  416             elif record.getParent() 
not in badParents:
 
  417                 references.append(record)
 
  422     def getExposure(self, dataRef):
 
  423         """Read input exposure for measurement. 
  427         dataRef : `lsst.daf.persistence.ButlerDataRef` 
  428             Butler data reference. 
  430         exposure = ForcedPhotImageTask.getExposure(self, dataRef)
 
  432         if self.
config.doApplyExternalPhotoCalib:
 
  433             source = f
"{self.config.externalPhotoCalibName}_photoCalib" 
  434             self.log.
info(
"Applying external photoCalib from %s", source)
 
  435             photoCalib = dataRef.get(source)
 
  436             exposure.setPhotoCalib(photoCalib)  
 
  438         if self.
config.doApplyExternalSkyWcs:
 
  439             source = f
"{self.config.externalSkyWcsName}_wcs" 
  440             self.log.
info(
"Applying external skyWcs from %s", source)
 
  441             skyWcs = dataRef.get(source)
 
  442             exposure.setWcs(skyWcs)
 
  444         if self.
config.doApplySkyCorr:
 
  445             self.log.
info(
"Apply sky correction")
 
  446             skyCorr = dataRef.get(
"skyCorr")
 
  447             exposure.maskedImage -= skyCorr.getImage()
 
  451     def _getConfigName(self):
 
  453         return self.dataPrefix + 
"forcedPhotCcd_config" 
  455     def _getMetadataName(self):
 
  457         return self.dataPrefix + 
"forcedPhotCcd_metadata" 
  460     def _makeArgumentParser(cls):
 
  462         parser.add_id_argument(
"--id", 
"forced_src", help=
"data ID with raw CCD keys [+ tract optionally], " 
  463                                "e.g. --id visit=12345 ccd=1,2 [tract=0]",
 
  464                                ContainerClass=PerTractCcdDataIdContainer)