LSSTApplications  18.0.0+106,18.0.0+50,19.0.0,19.0.0+1,19.0.0+10,19.0.0+11,19.0.0+13,19.0.0+17,19.0.0+2,19.0.0-1-g20d9b18+6,19.0.0-1-g425ff20,19.0.0-1-g5549ca4,19.0.0-1-g580fafe+6,19.0.0-1-g6fe20d0+1,19.0.0-1-g7011481+9,19.0.0-1-g8c57eb9+6,19.0.0-1-gb5175dc+11,19.0.0-1-gdc0e4a7+9,19.0.0-1-ge272bc4+6,19.0.0-1-ge3aa853,19.0.0-10-g448f008b,19.0.0-12-g6990b2c,19.0.0-2-g0d9f9cd+11,19.0.0-2-g3d9e4fb2+11,19.0.0-2-g5037de4,19.0.0-2-gb96a1c4+3,19.0.0-2-gd955cfd+15,19.0.0-3-g2d13df8,19.0.0-3-g6f3c7dc,19.0.0-4-g725f80e+11,19.0.0-4-ga671dab3b+1,19.0.0-4-gad373c5+3,19.0.0-5-ga2acb9c+2,19.0.0-5-gfe96e6c+2,w.2020.01
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 lsst.pipe.base import PipelineTaskConnections
36 from .forcedPhotImage import ForcedPhotImageTask, ForcedPhotImageConfig
37 
38 try:
39  from lsst.meas.mosaic import applyMosaicResults
40 except ImportError:
41  applyMosaicResults = None
42 
43 __all__ = ("PerTractCcdDataIdContainer", "ForcedPhotCcdConfig", "ForcedPhotCcdTask", "imageOverlapsTract")
44 
45 
47  """A data ID container which combines raw data IDs with a tract.
48 
49  Notes
50  -----
51  Required because we need to add "tract" to the raw data ID keys (defined as
52  whatever we use for ``src``) when no tract is provided (so that the user is
53  not required to know which tracts are spanned by the raw data ID).
54 
55  This subclass of `~lsst.pipe.base.DataIdContainer` assumes that a calexp is
56  being measured using the detection information, a set of reference
57  catalogs, from the set of coadds which intersect with the calexp. It needs
58  the calexp id (e.g. visit, raft, sensor), but is also uses the tract to
59  decide what set of coadds to use. The references from the tract whose
60  patches intersect with the calexp are used.
61  """
62 
63  def makeDataRefList(self, namespace):
64  """Make self.refList from self.idList
65  """
66  if self.datasetType is None:
67  raise RuntimeError("Must call setDatasetType first")
68  log = Log.getLogger("meas.base.forcedPhotCcd.PerTractCcdDataIdContainer")
69  skymap = None
70  visitTract = collections.defaultdict(set) # Set of tracts for each visit
71  visitRefs = collections.defaultdict(list) # List of data references for each visit
72  for dataId in self.idList:
73  if "tract" not in dataId:
74  # Discover which tracts the data overlaps
75  log.info("Reading WCS for components of dataId=%s to determine tracts", dict(dataId))
76  if skymap is None:
77  skymap = namespace.butler.get(namespace.config.coaddName + "Coadd_skyMap")
78 
79  for ref in namespace.butler.subset("calexp", dataId=dataId):
80  if not ref.datasetExists("calexp"):
81  continue
82 
83  visit = ref.dataId["visit"]
84  visitRefs[visit].append(ref)
85 
86  md = ref.get("calexp_md", immediate=True)
87  wcs = lsst.afw.geom.makeSkyWcs(md)
89  # Going with just the nearest tract. Since we're throwing all tracts for the visit
90  # together, this shouldn't be a problem unless the tracts are much smaller than a CCD.
91  tract = skymap.findTract(wcs.pixelToSky(box.getCenter()))
92  if imageOverlapsTract(tract, wcs, box):
93  visitTract[visit].add(tract.getId())
94  else:
95  self.refList.extend(ref for ref in namespace.butler.subset(self.datasetType, dataId=dataId))
96 
97  # Ensure all components of a visit are kept together by putting them all in the same set of tracts
98  for visit, tractSet in visitTract.items():
99  for ref in visitRefs[visit]:
100  for tract in tractSet:
101  self.refList.append(namespace.butler.dataRef(datasetType=self.datasetType,
102  dataId=ref.dataId, tract=tract))
103  if visitTract:
104  tractCounter = collections.Counter()
105  for tractSet in visitTract.values():
106  tractCounter.update(tractSet)
107  log.info("Number of visits for each tract: %s", dict(tractCounter))
108 
109 
110 def imageOverlapsTract(tract, imageWcs, imageBox):
111  """Return whether the given bounding box overlaps the tract given a WCS.
112 
113  Parameters
114  ----------
115  tract : `lsst.skymap.TractInfo`
116  TractInfo specifying a tract.
117  imageWcs : `lsst.afw.geom.SkyWcs`
118  World coordinate system for the image.
119  imageBox : `lsst.geom.Box2I`
120  Bounding box for the image.
121 
122  Returns
123  -------
124  overlap : `bool`
125  `True` if the bounding box overlaps the tract; `False` otherwise.
126  """
127  tractPoly = tract.getOuterSkyPolygon()
128 
129  imagePixelCorners = lsst.geom.Box2D(imageBox).getCorners()
130  try:
131  imageSkyCorners = imageWcs.pixelToSky(imagePixelCorners)
132  except lsst.pex.exceptions.LsstCppException as e:
133  # Protecting ourselves from awful Wcs solutions in input images
134  if (not isinstance(e.message, lsst.pex.exceptions.DomainErrorException) and
135  not isinstance(e.message, lsst.pex.exceptions.RuntimeErrorException)):
136  raise
137  return False
138 
139  imagePoly = lsst.sphgeom.ConvexPolygon.convexHull([coord.getVector() for coord in imageSkyCorners])
140  return tractPoly.intersects(imagePoly) # "intersects" also covers "contains" or "is contained by"
141 
142 
144  dimensions=("instrument", "visit", "detector", "skymap", "tract"),
145  defaultTemplates={"inputCoaddName": "deep",
146  "inputName": "calexp"}):
147  inputSchema = cT.InitInput(
148  doc="Schema for the input measurement catalogs.",
149  name="{inputCoaddName}Coadd_ref_schema",
150  storageClass="SourceCatalog",
151  )
152  outputSchema = cT.InitOutput(
153  doc="Schema for the output forced measurement catalogs.",
154  name="forced_src_schema",
155  storageClass="SourceCatalog",
156  )
157  exposure = cT.Input(
158  doc="Input exposure to perform photometry on.",
159  name="{inputName}",
160  storageClass="ExposureF",
161  dimensions=["instrument", "visit", "detector"],
162  )
163  refCat = cT.Input(
164  doc="Catalog of shapes and positions at which to force photometry.",
165  name="{inputCoaddName}Coadd_ref",
166  storageClass="SourceCatalog",
167  dimensions=["skymap", "tract", "patch"],
168  )
169  refWcs = cT.Input(
170  doc="Reference world coordinate system.",
171  name="{inputCoaddName}Coadd.wcs",
172  storageClass="Wcs",
173  dimensions=["abstract_filter", "skymap", "tract", "patch"],
174  )
175  measCat = cT.Output(
176  doc="Output forced photometry catalog.",
177  name="forced_src",
178  storageClass="SourceCatalog",
179  dimensions=["instrument", "visit", "detector", "skymap", "tract"],
180  )
181 
182 
183 class ForcedPhotCcdConfig(ForcedPhotImageConfig, pipelineConnections=ForcedPhotCcdConnections):
184  doApplyUberCal = lsst.pex.config.Field(
185  dtype=bool,
186  doc="Apply meas_mosaic ubercal results to input calexps?",
187  default=False
188  )
189 
190 
191 class ForcedPhotCcdTask(ForcedPhotImageTask):
192  """A command-line driver for performing forced measurement on CCD images.
193 
194  Notes
195  -----
196  This task is a subclass of
197  :lsst-task:`lsst.meas.base.forcedPhotImage.ForcedPhotImageTask` which is
198  specifically for doing forced measurement on a single CCD exposure, using
199  as a reference catalog the detections which were made on overlapping
200  coadds.
201 
202  The `run` method (inherited from `ForcedPhotImageTask`) takes a
203  `~lsst.daf.persistence.ButlerDataRef` argument that corresponds to a single
204  CCD. This should contain the data ID keys that correspond to the
205  ``forced_src`` dataset (the output dataset for this task), which are
206  typically all those used to specify the ``calexp`` dataset (``visit``,
207  ``raft``, ``sensor`` for LSST data) as well as a coadd tract. The tract is
208  used to look up the appropriate coadd measurement catalogs to use as
209  references (e.g. ``deepCoadd_src``; see
210  :lsst-task:`lsst.meas.base.references.CoaddSrcReferencesTask` for more
211  information). While the tract must be given as part of the dataRef, the
212  patches are determined automatically from the bounding box and WCS of the
213  calexp to be measured, and the filter used to fetch references is set via
214  the ``filter`` option in the configuration of
215  :lsst-task:`lsst.meas.base.references.BaseReferencesTask`).
216 
217  In addition to the `run` method, `ForcedPhotCcdTask` overrides several
218  methods of `ForcedPhotImageTask` to specialize it for single-CCD
219  processing, including `~ForcedPhotImageTask.makeIdFactory`,
220  `~ForcedPhotImageTask.fetchReferences`, and
221  `~ForcedPhotImageTask.getExposure`. None of these should be called
222  directly by the user, though it may be useful to override them further in
223  subclasses.
224  """
225 
226  ConfigClass = ForcedPhotCcdConfig
228  _DefaultName = "forcedPhotCcd"
229  dataPrefix = ""
230 
231  def runQuantum(self, butlerQC, inputRefs, outputRefs):
232  inputs = butlerQC.get(inputRefs)
233  inputs['refCat'] = self.filterReferences(inputs['exposure'], inputs['refCat'], inputs['refWcs'])
234  inputs['measCat'] = self.generateMeasCat(inputRefs.exposure.dataId,
235  inputs['exposure'],
236  inputs['refCat'], inputs['refWcs'],
237  "visit_detector")
238  outputs = self.run(**inputs)
239  butlerQC.put(outputs, outputRefs)
240 
241  def filterReferences(self, exposure, refCat, refWcs):
242  """Filter reference catalog so that all sources are within the
243  boundaries of the exposure.
244 
245  Parameters
246  ----------
247  exposure : `lsst.afw.image.exposure.Exposure`
248  Exposure to generate the catalog for.
249  refCat : `lsst.afw.table.SourceCatalog`
250  Catalog of shapes and positions at which to force photometry.
251  refWcs : `lsst.afw.image.SkyWcs`
252  Reference world coordinate system.
253 
254  Returns
255  -------
256  refSources : `lsst.afw.table.SourceCatalog`
257  Filtered catalog of forced sources to measure.
258 
259  Notes
260  -----
261  Filtering the reference catalog is currently handled by Gen2
262  specific methods. To function for Gen3, this method copies
263  code segments to do the filtering and transformation. The
264  majority of this code is based on the methods of
265  lsst.meas.algorithms.loadReferenceObjects.ReferenceObjectLoader
266 
267  """
268 
269  # Step 1: Determine bounds of the exposure photometry will
270  # be performed on.
271  expWcs = exposure.getWcs()
272  expRegion = exposure.getBBox(lsst.afw.image.PARENT)
273  expBBox = lsst.geom.Box2D(expRegion)
274  expBoxCorners = expBBox.getCorners()
275  expSkyCorners = [expWcs.pixelToSky(corner).getVector() for
276  corner in expBoxCorners]
277  expPolygon = lsst.sphgeom.ConvexPolygon(expSkyCorners)
278 
279  # Step 2: Filter out reference catalog sources that are
280  # not contained within the exposure boundaries.
281  sources = type(refCat)(refCat.table)
282  for record in refCat:
283  if expPolygon.contains(record.getCoord().getVector()):
284  sources.append(record)
285  refCatIdDict = {ref.getId(): ref.getParent() for ref in sources}
286 
287  # Step 3: Cull sources that do not have their parent
288  # source in the filtered catalog. Save two copies of each
289  # source.
290  refSources = type(refCat)(refCat.table)
291  for record in refCat:
292  if expPolygon.contains(record.getCoord().getVector()):
293  recordId = record.getId()
294  topId = recordId
295  while (topId > 0):
296  if topId in refCatIdDict:
297  topId = refCatIdDict[topId]
298  else:
299  break
300  if topId == 0:
301  refSources.append(record)
302 
303  # Step 4: Transform source footprints from the reference
304  # coordinates to the exposure coordinates.
305  for refRecord in refSources:
306  refRecord.setFootprint(refRecord.getFootprint().transform(refWcs,
307  expWcs, expRegion))
308  # Step 5: Replace reference catalog with filtered source list.
309  return refSources
310 
311  def makeIdFactory(self, dataRef):
312  """Create an object that generates globally unique source IDs.
313 
314  Source IDs are created based on a per-CCD ID and the ID of the CCD
315  itself.
316 
317  Parameters
318  ----------
319  dataRef : `lsst.daf.persistence.ButlerDataRef`
320  Butler data reference. The ``ccdExposureId_bits`` and
321  ``ccdExposureId`` datasets are accessed. The data ID must have the
322  keys that correspond to ``ccdExposureId``, which are generally the
323  same as those that correspond to ``calexp`` (``visit``, ``raft``,
324  ``sensor`` for LSST data).
325  """
326  expBits = dataRef.get("ccdExposureId_bits")
327  expId = int(dataRef.get("ccdExposureId"))
328  return lsst.afw.table.IdFactory.makeSource(expId, 64 - expBits)
329 
330  def getExposureId(self, dataRef):
331  return int(dataRef.get("ccdExposureId", immediate=True))
332 
333  def fetchReferences(self, dataRef, exposure):
334  """Get sources that overlap the exposure.
335 
336  Parameters
337  ----------
338  dataRef : `lsst.daf.persistence.ButlerDataRef`
339  Butler data reference corresponding to the image to be measured;
340  should have ``tract``, ``patch``, and ``filter`` keys.
341  exposure : `lsst.afw.image.Exposure`
342  The image to be measured (used only to obtain a WCS and bounding
343  box).
344 
345  Returns
346  -------
347  referencs : `lsst.afw.table.SourceCatalog`
348  Catalog of sources that overlap the exposure
349 
350  Notes
351  -----
352  The returned catalog is sorted by ID and guarantees that all included
353  children have their parent included and that all Footprints are valid.
354 
355  All work is delegated to the references subtask; see
356  :lsst-task:`lsst.meas.base.references.CoaddSrcReferencesTask`
357  for information about the default behavior.
358  """
359  references = lsst.afw.table.SourceCatalog(self.references.schema)
360  badParents = set()
361  unfiltered = self.references.fetchInBox(dataRef, exposure.getBBox(), exposure.getWcs())
362  for record in unfiltered:
363  if record.getFootprint() is None or record.getFootprint().getArea() == 0:
364  if record.getParent() != 0:
365  self.log.warn("Skipping reference %s (child of %s) with bad Footprint",
366  record.getId(), record.getParent())
367  else:
368  self.log.warn("Skipping reference parent %s with bad Footprint", record.getId())
369  badParents.add(record.getId())
370  elif record.getParent() not in badParents:
371  references.append(record)
372  # catalog must be sorted by parent ID for lsst.afw.table.getChildren to work
373  references.sort(lsst.afw.table.SourceTable.getParentKey())
374  return references
375 
376  def getExposure(self, dataRef):
377  """Read input exposure for measurement.
378 
379  Parameters
380  ----------
381  dataRef : `lsst.daf.persistence.ButlerDataRef`
382  Butler data reference. Only the ``calexp`` dataset is used, unless
383  ``config.doApplyUberCal`` is `True`, in which case the
384  corresponding meas_mosaic outputs are used as well.
385  """
386  exposure = ForcedPhotImageTask.getExposure(self, dataRef)
387  if not self.config.doApplyUberCal:
388  return exposure
389  if applyMosaicResults is None:
390  raise RuntimeError(
391  "Cannot use improved calibrations for %s because meas_mosaic could not be imported."
392  % (dataRef.dataId,))
393  else:
394  applyMosaicResults(dataRef, calexp=exposure)
395  return exposure
396 
397  def _getConfigName(self):
398  # Documented in superclass.
399  return self.dataPrefix + "forcedPhotCcd_config"
400 
401  def _getMetadataName(self):
402  # Documented in superclass
403  return self.dataPrefix + "forcedPhotCcd_metadata"
404 
405  @classmethod
406  def _makeArgumentParser(cls):
407  parser = lsst.pipe.base.ArgumentParser(name=cls._DefaultName)
408  parser.add_id_argument("--id", "forced_src", help="data ID with raw CCD keys [+ tract optionally], "
409  "e.g. --id visit=12345 ccd=1,2 [tract=0]",
410  ContainerClass=PerTractCcdDataIdContainer)
411  return parser
A floating-point coordinate rectangle geometry.
Definition: Box.h:413
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
Definition: functional.cc:33
daf::base::PropertySet * set
Definition: fits.cc:902
Definition: Log.h:706
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:163
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:516
static Key< RecordId > getParentKey()
Key for the parent ID.
Definition: Source.h:275
Backwards-compatibility support for depersisting the old Calib (FluxMag0/FluxMag0Err) objects...
lsst::geom::Box2I bboxFromMetadata(daf::base::PropertySet &metadata)
Determine the image bounding box from its metadata (FITS header)
Definition: Image.cc:694