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