lsst.meas.base  14.0-11-gfe379c9+1
forcedPhotCcd.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 #
3 # LSST Data Management System
4 # Copyright 2008-2015 AURA/LSST.
5 #
6 # This product includes software developed by the
7 # LSST Project (http://www.lsst.org/).
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 LSST License Statement and
20 # the GNU General Public License along with this program. If not,
21 # see <https://www.lsstcorp.org/LegalNotices/>.
22 #
23 import collections
24 
25 import lsst.pex.config
27 from lsst.log import Log
28 import lsst.pipe.base
29 import lsst.afw.image
30 import lsst.afw.table
31 from lsst.geom import convexHull
32 
33 from .forcedPhotImage import ForcedPhotImageTask, ForcedPhotImageConfig
34 
35 try:
36  from lsst.meas.mosaic import applyMosaicResults
37 except ImportError:
38  applyMosaicResults = None
39 
40 __all__ = ("PerTractCcdDataIdContainer", "ForcedPhotCcdConfig", "ForcedPhotCcdTask")
41 
42 
43 class PerTractCcdDataIdContainer(lsst.pipe.base.DataIdContainer):
44  """A version of lsst.pipe.base.DataIdContainer that combines raw data IDs with a tract.
45 
46  Required because we need to add "tract" to the raw data ID keys (defined as whatever we
47  use for 'src') when no tract is provided (so that the user is not required to know
48  which tracts are spanned by the raw data ID).
49 
50  This IdContainer assumes that a calexp is being measured using the detection information,
51  a set of reference catalogs, from the set of coadds which intersect with the calexp.
52  It needs the calexp id (e.g. visit, raft, sensor), but is also uses the tract to decide
53  what set of coadds to use. The references from the tract whose patches intersect with
54  the calexp are used.
55  """
56 
57  def makeDataRefList(self, namespace):
58  """Make self.refList from self.idList
59  """
60  if self.datasetType is None:
61  raise RuntimeError("Must call setDatasetType first")
62  log = Log.getLogger("meas.base.forcedPhotCcd.PerTractCcdDataIdContainer")
63  skymap = None
64  visitTract = collections.defaultdict(set) # Set of tracts for each visit
65  visitRefs = collections.defaultdict(list) # List of data references for each visit
66  for dataId in self.idList:
67  if "tract" not in dataId:
68  # Discover which tracts the data overlaps
69  log.info("Reading WCS for components of dataId=%s to determine tracts", dict(dataId))
70  if skymap is None:
71  skymap = namespace.butler.get(namespace.config.coaddName + "Coadd_skyMap")
72 
73  for ref in namespace.butler.subset("calexp", dataId=dataId):
74  if not ref.datasetExists("calexp"):
75  continue
76 
77  visit = ref.dataId["visit"]
78  visitRefs[visit].append(ref)
79 
80  md = ref.get("calexp_md", immediate=True)
81  wcs = lsst.afw.image.makeWcs(md)
83  # Going with just the nearest tract. Since we're throwing all tracts for the visit
84  # together, this shouldn't be a problem unless the tracts are much smaller than a CCD.
85  tract = skymap.findTract(wcs.pixelToSky(box.getCenter()))
86  if overlapsTract(tract, wcs, box):
87  visitTract[visit].add(tract.getId())
88  else:
89  self.refList.extend(ref for ref in namespace.butler.subset(self.datasetType, dataId=dataId))
90 
91  # Ensure all components of a visit are kept together by putting them all in the same set of tracts
92  for visit, tractSet in visitTract.items():
93  for ref in visitRefs[visit]:
94  for tract in tractSet:
95  self.refList.append(namespace.butler.dataRef(datasetType=self.datasetType,
96  dataId=ref.dataId, tract=tract))
97  if visitTract:
98  tractCounter = collections.Counter()
99  for tractSet in visitTract.values():
100  tractCounter.update(tractSet)
101  log.info("Number of visits for each tract: %s", dict(tractCounter))
102 
103 
104 def overlapsTract(tract, imageWcs, imageBox):
105  """Return whether the image (specified by Wcs and bounding box) overlaps the tract
106 
107  @param tract: TractInfo specifying a tract
108  @param imageWcs: Wcs for image
109  @param imageBox: Bounding box for image
110  @return bool
111  """
112  tractWcs = tract.getWcs()
113  tractCorners = [tractWcs.pixelToSky(lsst.afw.geom.Point2D(coord)).getVector() for
114  coord in tract.getBBox().getCorners()]
115  tractPoly = convexHull(tractCorners)
116 
117  try:
118  imageCorners = [imageWcs.pixelToSky(lsst.afw.geom.Point2D(pix)) for pix in imageBox.getCorners()]
119  except lsst.pex.exceptions.LsstCppException as e:
120  # Protecting ourselves from awful Wcs solutions in input images
121  if (not isinstance(e.message, lsst.pex.exceptions.DomainErrorException) and
122  not isinstance(e.message, lsst.pex.exceptions.RuntimeErrorException)):
123  raise
124  return False
125 
126  imagePoly = convexHull([coord.getVector() for coord in imageCorners])
127  if imagePoly is None:
128  return False
129  return tractPoly.intersects(imagePoly) # "intersects" also covers "contains" or "is contained by"
130 
131 
133  doApplyUberCal = lsst.pex.config.Field(
134  dtype=bool,
135  doc="Apply meas_mosaic ubercal results to input calexps?",
136  default=False
137  )
138 
139 
145 
146 
148  """!A command-line driver for performing forced measurement on CCD images
149 
150  This task is a subclass of ForcedPhotImageTask which is specifically for doing forced
151  measurement on a single CCD exposure, using as a reference catalog the detections which
152  were made on overlapping coadds.
153 
154  The run method (inherited from ForcedPhotImageTask) takes a lsst.daf.persistence.ButlerDataRef
155  argument that corresponds to a single CCD. This should contain the data ID keys that correspond to
156  the "forced_src" dataset (the output dataset for ForcedPhotCcdTask), which are typically all those
157  used to specify the "calexp" dataset (e.g. visit, raft, sensor for LSST data) as well as a coadd
158  tract. The tract is used to look up the appropriate coadd measurement catalogs to use as references
159  (e.g. deepCoadd_src; see CoaddSrcReferencesTask for more information). While the tract must be given
160  as part of the dataRef, the patches are determined automatically from the bounding box and WCS of the
161  calexp to be measured, and the filter used to fetch references is set via config
162  (BaseReferencesConfig.filter).
163 
164  In addition to the run method, ForcedPhotCcdTask overrides several methods of ForcedPhotImageTask
165  to specialize it for single-CCD processing, including makeIdFactory(), fetchReferences(), and
166  getExposure(). None of these should be called directly by the user, though it may be useful
167  to override them further in subclasses.
168  """
169 
170  ConfigClass = ForcedPhotCcdConfig
171  RunnerClass = lsst.pipe.base.ButlerInitializedTaskRunner
172  _DefaultName = "forcedPhotCcd"
173  dataPrefix = ""
174 
175  def makeIdFactory(self, dataRef):
176  """Create an object that generates globally unique source IDs from per-CCD IDs and the CCD ID.
177 
178  @param dataRef Data reference from butler. The "ccdExposureId_bits" and "ccdExposureId"
179  datasets are accessed. The data ID must have the keys that correspond
180  to ccdExposureId, which is generally the same that correspond to "calexp"
181  (e.g. visit, raft, sensor for LSST data).
182  """
183  expBits = dataRef.get("ccdExposureId_bits")
184  expId = int(dataRef.get("ccdExposureId"))
185  return lsst.afw.table.IdFactory.makeSource(expId, 64 - expBits)
186 
187  def getExposureId(self, dataRef):
188  return int(dataRef.get("ccdExposureId", immediate=True))
189 
190  def fetchReferences(self, dataRef, exposure):
191  """Return a SourceCatalog of sources which overlap the exposure.
192 
193  The returned catalog is sorted by ID and guarantees that all included children have their
194  parent included and that all Footprints are valid.
195 
196  @param dataRef Data reference from butler corresponding to the image to be measured;
197  should have tract, patch, and filter keys.
198  @param exposure lsst.afw.image.Exposure to be measured (used only to obtain a Wcs and
199  bounding box).
200 
201  All work is delegated to the references subtask; see CoaddSrcReferencesTask for information
202  about the default behavior.
203  """
204  references = lsst.afw.table.SourceCatalog(self.references.schema)
205  badParents = set()
206  unfiltered = self.references.fetchInBox(dataRef, exposure.getBBox(), exposure.getWcs())
207  for record in unfiltered:
208  if record.getFootprint() is None or record.getFootprint().getArea() == 0:
209  if record.getParent() != 0:
210  self.log.warn("Skipping reference %s (child of %s) with bad Footprint",
211  record.getId(), record.getParent())
212  else:
213  self.log.warn("Skipping reference parent %s with bad Footprint", record.getId())
214  badParents.add(record.getId())
215  elif record.getParent() not in badParents:
216  references.append(record)
217  # catalog must be sorted by parent ID for lsst.afw.table.getChildren to work
218  references.sort(lsst.afw.table.SourceTable.getParentKey())
219  return references
220 
221  def getExposure(self, dataRef):
222  """Read input exposure to measure
223 
224  @param dataRef Data reference from butler. Only the 'calexp' dataset is used,
225  unless config.doApplyUberCal is true, in which case the corresponding
226  meas_mosaic outputs are used as well.
227  """
228  exposure = ForcedPhotImageTask.getExposure(self, dataRef)
229  if not self.config.doApplyUberCal:
230  return exposure
231  if applyMosaicResults is None:
232  raise RuntimeError(
233  "Cannot use improved calibrations for %s because meas_mosaic could not be imported."
234  % (dataRef.dataId,))
235  else:
236  applyMosaicResults(dataRef, calexp=exposure)
237  return exposure
238 
239  def _getConfigName(self):
240  """!Return the name of the config dataset. Forces config comparison from run-to-run
241  """
242  return self.dataPrefix + "forcedPhotCcd_config"
243 
244  def _getMetadataName(self):
245  """!Return the name of the metadata dataset. Forced metadata to be saved
246  """
247  return self.dataPrefix + "forcedPhotCcd_metadata"
248 
249  @classmethod
250  def _makeArgumentParser(cls):
251  parser = lsst.pipe.base.ArgumentParser(name=cls._DefaultName)
252  parser.add_id_argument("--id", "forced_src", help="data ID with raw CCD keys [+ tract optionally], "
253  "e.g. --id visit=12345 ccd=1,2 [tract=0]",
254  ContainerClass=PerTractCcdDataIdContainer)
255  return parser
A base class for command-line forced measurement drivers.
std::shared_ptr< Wcs > makeWcs(coord::Coord const &crval, geom::Point2D const &crpix, double CD11, double CD12, double CD21, double CD22)
A command-line driver for performing forced measurement on CCD images.
static std::shared_ptr< IdFactory > makeSource(RecordId expId, int reserved)
geom::Box2I bboxFromMetadata(daf::base::PropertySet &metadata)
def overlapsTract(tract, imageWcs, imageBox)
static Key< RecordId > getParentKey()
def fetchReferences(self, dataRef, exposure)
Config class for forced measurement driver task.