LSSTApplications  11.0-13-gbb96280,12.1.rc1,12.1.rc1+1,12.1.rc1+2,12.1.rc1+5,12.1.rc1+8,12.1.rc1-1-g06d7636+1,12.1.rc1-1-g253890b+5,12.1.rc1-1-g3d31b68+7,12.1.rc1-1-g3db6b75+1,12.1.rc1-1-g5c1385a+3,12.1.rc1-1-g83b2247,12.1.rc1-1-g90cb4cf+6,12.1.rc1-1-g91da24b+3,12.1.rc1-2-g3521f8a,12.1.rc1-2-g39433dd+4,12.1.rc1-2-g486411b+2,12.1.rc1-2-g4c2be76,12.1.rc1-2-gc9c0491,12.1.rc1-2-gda2cd4f+6,12.1.rc1-3-g3391c73+2,12.1.rc1-3-g8c1bd6c+1,12.1.rc1-3-gcf4b6cb+2,12.1.rc1-4-g057223e+1,12.1.rc1-4-g19ed13b+2,12.1.rc1-4-g30492a7
LSSTDataManagementBasePackage
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 _addDataRef(self, namespace, dataId, tract):
58  """Construct a dataRef based on dataId, but with an added tract key"""
59  forcedDataId = dataId.copy()
60  forcedDataId['tract'] = tract
61  dataRef = namespace.butler.dataRef(datasetType=self.datasetType, dataId=forcedDataId)
62  self.refList.append(dataRef)
63 
64  def makeDataRefList(self, namespace):
65  """Make self.refList from self.idList
66  """
67  if self.datasetType is None:
68  raise RuntimeError("Must call setDatasetType first")
69  log = Log.getLogger("meas.base.forcedPhotCcd.PerTractCcdDataIdContainer")
70  skymap = None
71  visitTract = collections.defaultdict(set) # Set of tracts for each visit
72  visitRefs = collections.defaultdict(list) # List of data references for each visit
73  for dataId in self.idList:
74  if "tract" not in dataId:
75  # Discover which tracts the data overlaps
76  log.info("Reading WCS for components of dataId=%s to determine tracts", dict(dataId))
77  if skymap is None:
78  skymap = namespace.butler.get(namespace.config.coaddName + "Coadd_skyMap")
79 
80  for ref in namespace.butler.subset("calexp", dataId=dataId):
81  if not ref.datasetExists("calexp"):
82  continue
83 
84  visit = ref.dataId["visit"]
85  visitRefs[visit].append(ref)
86 
87  md = ref.get("calexp_md", immediate=True)
88  wcs = lsst.afw.image.makeWcs(md)
90  lsst.afw.geom.Point2D(md.get("NAXIS1"), md.get("NAXIS2")))
91  # Going with just the nearest tract. Since we're throwing all tracts for the visit
92  # together, this shouldn't be a problem unless the tracts are much smaller than a CCD.
93  tract = skymap.findTract(wcs.pixelToSky(box.getCenter()))
94  if overlapsTract(tract, wcs, box):
95  visitTract[visit].add(tract.getId())
96  else:
97  tract = dataId.pop("tract")
98  # making a DataRef for src fills out any missing keys and allows us to iterate
99  for ref in namespace.butler.subset("src", dataId=dataId):
100  self._addDataRef(namespace, ref.dataId, tract)
101 
102  # Ensure all components of a visit are kept together by putting them all in the same set of tracts
103  for visit, tractSet in visitTract.items():
104  for ref in visitRefs[visit]:
105  for tract in tractSet:
106  self._addDataRef(namespace, ref.dataId, tract)
107  if visitTract:
108  tractCounter = collections.Counter()
109  for tractSet in visitTract.values():
110  tractCounter.update(tractSet)
111  log.info("Number of visits for each tract: %s", dict(tractCounter))
112 
113 
114 def overlapsTract(tract, imageWcs, imageBox):
115  """Return whether the image (specified by Wcs and bounding box) overlaps the tract
116 
117  @param tract: TractInfo specifying a tract
118  @param imageWcs: Wcs for image
119  @param imageBox: Bounding box for image
120  @return bool
121  """
122  tractWcs = tract.getWcs()
123  tractCorners = [tractWcs.pixelToSky(lsst.afw.geom.Point2D(coord)).getVector() for
124  coord in tract.getBBox().getCorners()]
125  tractPoly = convexHull(tractCorners)
126 
127  try:
128  imageCorners = [imageWcs.pixelToSky(lsst.afw.geom.Point2D(pix)) for pix in imageBox.getCorners()]
129  except lsst.pex.exceptions.LsstCppException as e:
130  # Protecting ourselves from awful Wcs solutions in input images
131  if (not isinstance(e.message, lsst.pex.exceptions.DomainErrorException) and
132  not isinstance(e.message, lsst.pex.exceptions.RuntimeErrorException)):
133  raise
134  return False
135 
136  imagePoly = convexHull([coord.getVector() for coord in imageCorners])
137  if imagePoly is None:
138  return False
139  return tractPoly.intersects(imagePoly) # "intersects" also covers "contains" or "is contained by"
140 
141 
142 class ForcedPhotCcdConfig(ForcedPhotImageConfig):
143  doApplyUberCal = lsst.pex.config.Field(
144  dtype=bool,
145  doc="Apply meas_mosaic ubercal results to input calexps?",
146  default=False
147  )
148 
149 ## @addtogroup LSST_task_documentation
150 ## @{
151 ## @page processForcedCcdTask
152 ## ForcedPhotCcdTask
153 ## @copybrief ForcedPhotCcdTask
154 ## @}
155 
156 
157 class ForcedPhotCcdTask(ForcedPhotImageTask):
158  """!A command-line driver for performing forced measurement on CCD images
159 
160  This task is a subclass of ForcedPhotImageTask which is specifically for doing forced
161  measurement on a single CCD exposure, using as a reference catalog the detections which
162  were made on overlapping coadds.
163 
164  The run method (inherited from ForcedPhotImageTask) takes a lsst.daf.persistence.ButlerDataRef
165  argument that corresponds to a single CCD. This should contain the data ID keys that correspond to
166  the "forced_src" dataset (the output dataset for ForcedPhotCcdTask), which are typically all those
167  used to specify the "calexp" dataset (e.g. visit, raft, sensor for LSST data) as well as a coadd
168  tract. The tract is used to look up the appropriate coadd measurement catalogs to use as references
169  (e.g. deepCoadd_src; see CoaddSrcReferencesTask for more information). While the tract must be given
170  as part of the dataRef, the 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 config
172  (BaseReferencesConfig.filter).
173 
174  In addition to the run method, ForcedPhotCcdTask overrides several methods of ForcedPhotImageTask
175  to specialize it for single-CCD processing, including makeIdFactory(), fetchReferences(), and
176  getExposure(). None of these should be called directly by the user, though it may be useful
177  to override them further in subclasses.
178  """
179 
180  ConfigClass = ForcedPhotCcdConfig
181  RunnerClass = lsst.pipe.base.ButlerInitializedTaskRunner
182  _DefaultName = "forcedPhotCcd"
183  dataPrefix = ""
184 
185  def makeIdFactory(self, dataRef):
186  """Create an object that generates globally unique source IDs from per-CCD IDs and the CCD ID.
187 
188  @param dataRef Data reference from butler. The "ccdExposureId_bits" and "ccdExposureId"
189  datasets are accessed. The data ID must have the keys that correspond
190  to ccdExposureId, which is generally the same that correspond to "calexp"
191  (e.g. visit, raft, sensor for LSST data).
192  """
193  expBits = dataRef.get("ccdExposureId_bits")
194  expId = int(dataRef.get("ccdExposureId"))
195  return lsst.afw.table.IdFactory.makeSource(expId, 64 - expBits)
196 
197  def getExposureId(self, dataRef):
198  return int(dataRef.get("ccdExposureId", immediate=True))
199 
200  def fetchReferences(self, dataRef, exposure):
201  """Return a SourceCatalog of sources which overlap the exposure.
202 
203  The returned catalog is sorted by ID and guarantees that all included children have their
204  parent included and that all Footprints are valid.
205 
206  @param dataRef Data reference from butler corresponding to the image to be measured;
207  should have tract, patch, and filter keys.
208  @param exposure lsst.afw.image.Exposure to be measured (used only to obtain a Wcs and
209  bounding box).
210 
211  All work is delegated to the references subtask; see CoaddSrcReferencesTask for information
212  about the default behavior.
213  """
214  references = lsst.afw.table.SourceCatalog(self.references.schema)
215  badParents = set()
216  unfiltered = self.references.fetchInBox(dataRef, exposure.getBBox(), exposure.getWcs())
217  for record in unfiltered:
218  if record.getFootprint() is None or record.getFootprint().getArea() == 0:
219  if record.getParent() != 0:
220  self.log.warn("Skipping reference %s (child of %s) with bad Footprint",
221  record.getId(), record.getParent())
222  else:
223  self.log.warn("Skipping reference parent %s with bad Footprint", record.getId())
224  badParents.add(record.getId())
225  elif record.getParent() not in badParents:
226  references.append(record)
227  # catalog must be sorted by parent ID for lsst.afw.table.getChildren to work
228  references.sort(lsst.afw.table.SourceTable.getParentKey())
229  return references
230 
231  def getExposure(self, dataRef):
232  """Read input exposure to measure
233 
234  @param dataRef Data reference from butler. Only the 'calexp' dataset is used,
235  unless config.doApplyUberCal is true, in which case the corresponding
236  meas_mosaic outputs are used as well.
237  """
238  exposure = ForcedPhotImageTask.getExposure(self, dataRef)
239  if not self.config.doApplyUberCal:
240  return exposure
241  if applyMosaicResults is None:
242  raise RuntimeError(
243  "Cannot use improved calibrations for %s because meas_mosaic could not be imported."
244  % (dataRef.dataId,))
245  else:
246  applyMosaicResults(dataRef, calexp=exposure)
247  return exposure
248 
249  def _getConfigName(self):
250  """!Return the name of the config dataset. Forces config comparison from run-to-run
251  """
252  return self.dataPrefix + "forcedPhotCcd_config"
253 
254  def _getMetadataName(self):
255  """!Return the name of the metadata dataset. Forced metadata to be saved
256  """
257  return self.dataPrefix + "forcedPhotCcd_metadata"
258 
259  @classmethod
261  parser = lsst.pipe.base.ArgumentParser(name=cls._DefaultName)
262  parser.add_id_argument("--id", "forced_src", help="data ID with raw CCD keys [+ tract optionally], "
263  "e.g. --id visit=12345 ccd=1,2 [tract=0]",
264  ContainerClass=PerTractCcdDataIdContainer)
265  return parser
static boost::shared_ptr< IdFactory > makeSource(RecordId expId, int reserved)
Return an IdFactory that includes another, fixed ID in the higher-order bits.
A command-line driver for performing forced measurement on CCD images.
def _getConfigName
Return the name of the config dataset.
Definition: Log.h:716
Custom catalog class for record/table subclasses that are guaranteed to have an ID, and should generally be sorted by that ID.
Definition: fwd.h:55
boost::shared_ptr< Wcs > makeWcs(coord::Coord const &crval, geom::Point2D const &crpix, double CD11, double CD12, double CD21, double CD22)
Create a Wcs object from crval, crpix, CD, using CD elements (useful from python) ...
Definition: makeWcs.cc:138
def _getMetadataName
Return the name of the metadata dataset.
static Key< RecordId > getParentKey()
Key for the parent ID.
Definition: Source.h:258
A floating-point coordinate rectangle geometry.
Definition: Box.h:271