LSSTApplications  18.1.0
LSSTDataManagementBasePackage
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 
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  def setDefaults(self):
149  super().setDefaults()
150 
151  # Override the Gen3 datasets from ForcedPhotImageTaskConfig.
152  # When these nameTemplate values are used in ForcedPhotCoadd,
153  # there is a coadd name that may change, depending on the
154  # coadd type. For CCD forced photometry, there will likely
155  # only ever be a single calexp type, but for consistency, use
156  # the nameTemplate with nothing to substitute.
157  self.outputSchema.nameTemplate = "forced_src_schema"
158  self.exposure.nameTemplate = "{inputName}"
159  self.exposure.dimensions = ["instrument", "visit", "detector"]
160  self.measCat.nameTemplate = "forced_src"
161  self.measCat.dimensions = ["instrument", "visit", "detector", "skymap", "tract"]
162 
163  self.formatTemplateNames({"inputName": "calexp",
164  "inputCoaddName": "deep"})
165  self.quantum.dimensions = ("instrument", "visit", "detector", "skymap", "tract")
166 
167 
169  """A command-line driver for performing forced measurement on CCD images.
170 
171  Notes
172  -----
173  This task is a subclass of
174  :lsst-task:`lsst.meas.base.forcedPhotImage.ForcedPhotImageTask` which is
175  specifically for doing forced measurement on a single CCD exposure, using
176  as a reference catalog the detections which were made on overlapping
177  coadds.
178 
179  The `run` method (inherited from `ForcedPhotImageTask`) takes a
180  `~lsst.daf.persistence.ButlerDataRef` argument that corresponds to a single
181  CCD. This should contain the data ID keys that correspond to the
182  ``forced_src`` dataset (the output dataset for this task), which are
183  typically all those used to specify the ``calexp`` dataset (``visit``,
184  ``raft``, ``sensor`` for LSST data) as well as a coadd tract. The tract is
185  used to look up the appropriate coadd measurement catalogs to use as
186  references (e.g. ``deepCoadd_src``; see
187  :lsst-task:`lsst.meas.base.references.CoaddSrcReferencesTask` for more
188  information). While the tract must be given as part of the dataRef, the
189  patches are determined automatically from the bounding box and WCS of the
190  calexp to be measured, and the filter used to fetch references is set via
191  the ``filter`` option in the configuration of
192  :lsst-task:`lsst.meas.base.references.BaseReferencesTask`).
193 
194  In addition to the `run` method, `ForcedPhotCcdTask` overrides several
195  methods of `ForcedPhotImageTask` to specialize it for single-CCD
196  processing, including `~ForcedPhotImageTask.makeIdFactory`,
197  `~ForcedPhotImageTask.fetchReferences`, and
198  `~ForcedPhotImageTask.getExposure`. None of these should be called
199  directly by the user, though it may be useful to override them further in
200  subclasses.
201  """
202 
203  ConfigClass = ForcedPhotCcdConfig
205  _DefaultName = "forcedPhotCcd"
206  dataPrefix = ""
207 
208  def adaptArgsAndRun(self, inputData, inputDataIds, outputDataIds, butler):
209  inputData['refWcs'] = butler.get(f"{self.config.refWcs.name}.wcs", inputDataIds["refWcs"])
210  inputData['refCat'] = self.filterReferences(inputData['exposure'],
211  inputData['refCat'], inputData['refWcs'])
212  inputData['measCat'] = self.generateMeasCat(inputDataIds['exposure'],
213  inputData['exposure'],
214  inputData['refCat'], inputData['refWcs'],
215  "visit_detector", butler)
216 
217  return self.run(**inputData)
218 
219  def filterReferences(self, exposure, refCat, refWcs):
220  """Filter reference catalog so that all sources are within the
221  boundaries of the exposure.
222 
223  Parameters
224  ----------
225  exposure : `lsst.afw.image.exposure.Exposure`
226  Exposure to generate the catalog for.
227  refCat : `lsst.afw.table.SourceCatalog`
228  Catalog of shapes and positions at which to force photometry.
229  refWcs : `lsst.afw.image.SkyWcs`
230  Reference world coordinate system.
231 
232  Returns
233  -------
234  refSources : `lsst.afw.table.SourceCatalog`
235  Filtered catalog of forced sources to measure.
236 
237  Notes
238  -----
239  Filtering the reference catalog is currently handled by Gen2
240  specific methods. To function for Gen3, this method copies
241  code segments to do the filtering and transformation. The
242  majority of this code is based on the methods of
243  lsst.meas.algorithms.loadReferenceObjects.ReferenceObjectLoader
244 
245  """
246 
247  # Step 1: Determine bounds of the exposure photometry will
248  # be performed on.
249  expWcs = exposure.getWcs()
250  expRegion = exposure.getBBox(lsst.afw.image.PARENT)
251  expBBox = lsst.geom.Box2D(expRegion)
252  expBoxCorners = expBBox.getCorners()
253  expSkyCorners = [expWcs.pixelToSky(corner).getVector() for
254  corner in expBoxCorners]
255  expPolygon = lsst.sphgeom.ConvexPolygon(expSkyCorners)
256 
257  # Step 2: Filter out reference catalog sources that are
258  # not contained within the exposure boundaries.
259  sources = type(refCat)(refCat.table)
260  for record in refCat:
261  if expPolygon.contains(record.getCoord().getVector()):
262  sources.append(record)
263  refCatIdDict = {ref.getId(): ref.getParent() for ref in sources}
264 
265  # Step 3: Cull sources that do not have their parent
266  # source in the filtered catalog. Save two copies of each
267  # source.
268  refSources = type(refCat)(refCat.table)
269  for record in refCat:
270  if expPolygon.contains(record.getCoord().getVector()):
271  recordId = record.getId()
272  topId = recordId
273  while (topId > 0):
274  if topId in refCatIdDict:
275  topId = refCatIdDict[topId]
276  else:
277  break
278  if topId == 0:
279  refSources.append(record)
280 
281  # Step 4: Transform source footprints from the reference
282  # coordinates to the exposure coordinates.
283  for refRecord in refSources:
284  refRecord.setFootprint(refRecord.getFootprint().transform(refWcs,
285  expWcs, expRegion))
286  # Step 5: Replace reference catalog with filtered source list.
287  return refSources
288 
289  def makeIdFactory(self, dataRef):
290  """Create an object that generates globally unique source IDs.
291 
292  Source IDs are created based on a per-CCD ID and the ID of the CCD
293  itself.
294 
295  Parameters
296  ----------
297  dataRef : `lsst.daf.persistence.ButlerDataRef`
298  Butler data reference. The ``ccdExposureId_bits`` and
299  ``ccdExposureId`` datasets are accessed. The data ID must have the
300  keys that correspond to ``ccdExposureId``, which are generally the
301  same as those that correspond to ``calexp`` (``visit``, ``raft``,
302  ``sensor`` for LSST data).
303  """
304  expBits = dataRef.get("ccdExposureId_bits")
305  expId = int(dataRef.get("ccdExposureId"))
306  return lsst.afw.table.IdFactory.makeSource(expId, 64 - expBits)
307 
308  def getExposureId(self, dataRef):
309  return int(dataRef.get("ccdExposureId", immediate=True))
310 
311  def fetchReferences(self, dataRef, exposure):
312  """Get sources that overlap the exposure.
313 
314  Parameters
315  ----------
316  dataRef : `lsst.daf.persistence.ButlerDataRef`
317  Butler data reference corresponding to the image to be measured;
318  should have ``tract``, ``patch``, and ``filter`` keys.
319  exposure : `lsst.afw.image.Exposure`
320  The image to be measured (used only to obtain a WCS and bounding
321  box).
322 
323  Returns
324  -------
325  referencs : `lsst.afw.table.SourceCatalog`
326  Catalog of sources that overlap the exposure
327 
328  Notes
329  -----
330  The returned catalog is sorted by ID and guarantees that all included
331  children have their parent included and that all Footprints are valid.
332 
333  All work is delegated to the references subtask; see
334  :lsst-task:`lsst.meas.base.references.CoaddSrcReferencesTask`
335  for information about the default behavior.
336  """
337  references = lsst.afw.table.SourceCatalog(self.references.schema)
338  badParents = set()
339  unfiltered = self.references.fetchInBox(dataRef, exposure.getBBox(), exposure.getWcs())
340  for record in unfiltered:
341  if record.getFootprint() is None or record.getFootprint().getArea() == 0:
342  if record.getParent() != 0:
343  self.log.warn("Skipping reference %s (child of %s) with bad Footprint",
344  record.getId(), record.getParent())
345  else:
346  self.log.warn("Skipping reference parent %s with bad Footprint", record.getId())
347  badParents.add(record.getId())
348  elif record.getParent() not in badParents:
349  references.append(record)
350  # catalog must be sorted by parent ID for lsst.afw.table.getChildren to work
351  references.sort(lsst.afw.table.SourceTable.getParentKey())
352  return references
353 
354  def getExposure(self, dataRef):
355  """Read input exposure for measurement.
356 
357  Parameters
358  ----------
359  dataRef : `lsst.daf.persistence.ButlerDataRef`
360  Butler data reference. Only the ``calexp`` dataset is used, unless
361  ``config.doApplyUberCal`` is `True`, in which case the
362  corresponding meas_mosaic outputs are used as well.
363  """
364  exposure = ForcedPhotImageTask.getExposure(self, dataRef)
365  if not self.config.doApplyUberCal:
366  return exposure
367  if applyMosaicResults is None:
368  raise RuntimeError(
369  "Cannot use improved calibrations for %s because meas_mosaic could not be imported."
370  % (dataRef.dataId,))
371  else:
372  applyMosaicResults(dataRef, calexp=exposure)
373  return exposure
374 
375  def _getConfigName(self):
376  # Documented in superclass.
377  return self.dataPrefix + "forcedPhotCcd_config"
378 
379  def _getMetadataName(self):
380  # Documented in superclass
381  return self.dataPrefix + "forcedPhotCcd_metadata"
382 
383  @classmethod
384  def _makeArgumentParser(cls):
386  parser.add_id_argument("--id", "forced_src", help="data ID with raw CCD keys [+ tract optionally], "
387  "e.g. --id visit=12345 ccd=1,2 [tract=0]",
388  ContainerClass=PerTractCcdDataIdContainer)
389  return parser
A floating-point coordinate rectangle geometry.
Definition: Box.h:305
def adaptArgsAndRun(self, inputData, inputDataIds, outputDataIds, butler)
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
Definition: functional.cc:33
def formatTemplateNames(self, templateParamsDict)
Definition: config.py:326
daf::base::PropertySet * set
Definition: fits.cc:884
def run(self, measCat, exposure, refCat, refWcs, exposureId=None)
Definition: Log.h:691
static std::shared_ptr< IdFactory > makeSource(RecordId expId, int reserved)
Return an IdFactory that includes another, fixed ID in the higher-order bits.
Definition: IdFactory.cc:72
static ConvexPolygon convexHull(std::vector< UnitVector3d > const &points)
convexHull returns the convex hull of the given set of points if it exists and throws an exception ot...
Definition: ConvexPolygon.h:65
table::Key< int > type
Definition: Detector.cc:167
def generateMeasCat(self, exposureDataId, exposure, refCat, refWcs, idPackerName, butler)
def imageOverlapsTract(tract, imageWcs, imageBox)
ConvexPolygon is a closed convex polygon on the unit sphere.
Definition: ConvexPolygon.h:57
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")
Construct a FITS SkyWcs from camera geometry.
Definition: SkyWcs.cc:496
def filterReferences(self, exposure, refCat, refWcs)
static Key< RecordId > getParentKey()
Key for the parent ID.
Definition: Source.h:277
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects...
def fetchReferences(self, dataRef, exposure)
lsst::geom::Box2I bboxFromMetadata(daf::base::PropertySet &metadata)
Determine the image bounding box from its metadata (FITS header)
Definition: Image.cc:709