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