LSSTApplications  18.1.0
LSSTDataManagementBasePackage
makeDiscreteSkyMap.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2008-2015 AURA/LSST.
4 #
5 # This product includes software developed by the
6 # LSST Project (http://www.lsst.org/).
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the LSST License Statement and
19 # the GNU General Public License along with this program. If not,
20 # see <https://www.lsstcorp.org/LegalNotices/>.
21 #
22 import sys
23 import traceback
24 import lsst.sphgeom
25 
26 import lsst.afw.image as afwImage
27 import lsst.afw.geom as afwGeom
28 import lsst.pex.config as pexConfig
29 import lsst.pipe.base as pipeBase
30 from lsst.skymap import DiscreteSkyMap, BaseSkyMap
31 from lsst.pipe.base import ArgumentParser
32 
33 
34 class MakeDiscreteSkyMapConfig(pexConfig.Config):
35  """Config for MakeDiscreteSkyMapTask
36  """
37  coaddName = pexConfig.Field(
38  doc="coadd name, e.g. deep, goodSeeing, chiSquared",
39  dtype=str,
40  default="deep",
41  )
42  skyMap = pexConfig.ConfigField(
43  dtype=BaseSkyMap.ConfigClass,
44  doc="SkyMap configuration parameters, excluding position and radius"
45  )
46  borderSize = pexConfig.Field(
47  doc="additional border added to the bounding box of the calexps, in degrees",
48  dtype=float,
49  default=0.0
50  )
51  doAppend = pexConfig.Field(
52  doc="append another tract to an existing DiscreteSkyMap on disk, if present?",
53  dtype=bool,
54  default=False
55  )
56  doWrite = pexConfig.Field(
57  doc="persist the skyMap?",
58  dtype=bool,
59  default=True,
60  )
61 
62  def setDefaults(self):
63  self.skyMap.tractOverlap = 0.0
64 
65 
66 class MakeDiscreteSkyMapRunner(pipeBase.TaskRunner):
67  """Run a task with all dataRefs at once, rather than one dataRef at a time.
68 
69  Call the run method of the task using two positional arguments:
70  - butler: data butler
71  - dataRefList: list of all dataRefs,
72  """
73  @staticmethod
74  def getTargetList(parsedCmd):
75  return [(parsedCmd.butler, parsedCmd.id.refList)]
76 
77  def __call__(self, args):
78  """
79  @param args Arguments for Task.run()
80 
81  @return:
82  - None if self.doReturnResults false
83  - A pipe_base Struct containing these fields if self.doReturnResults true:
84  - dataRef: the provided data reference
85  - metadata: task metadata after execution of run
86  - result: result returned by task run, or None if the task fails
87  """
88  butler, dataRefList = args
89  task = self.TaskClass(config=self.config, log=self.log)
90  result = None # in case the task fails
91  exitStatus = 0 # exit status for shell
92  if self.doRaise:
93  result = task.runDataRef(butler, dataRefList)
94  else:
95  try:
96  result = task.runDataRef(butler, dataRefList)
97  except Exception as e:
98  task.log.fatal("Failed: %s" % e)
99  exitStatus = 1
100  if not isinstance(e, pipeBase.TaskError):
101  traceback.print_exc(file=sys.stderr)
102  for dataRef in dataRefList:
103  task.writeMetadata(dataRef)
104 
105  if self.doReturnResults:
106  return pipeBase.Struct(
107  dataRefList=dataRefList,
108  metadata=task.metadata,
109  result=result,
110  exitStatus=exitStatus,
111  )
112  else:
113  return pipeBase.Struct(
114  exitStatus=exitStatus,
115  )
116 
117 
118 class MakeDiscreteSkyMapTask(pipeBase.CmdLineTask):
119  """!Make a DiscreteSkyMap in a repository, using the bounding box of a set of calexps.
120 
121  The command-line and run signatures and config are sufficiently different from MakeSkyMapTask
122  that we don't inherit from it, but it is a replacement, so we use the same config/metadata names.
123  """
124  ConfigClass = MakeDiscreteSkyMapConfig
125  _DefaultName = "makeDiscreteSkyMap"
126  RunnerClass = MakeDiscreteSkyMapRunner
127 
128  def __init__(self, **kwargs):
129  pipeBase.CmdLineTask.__init__(self, **kwargs)
130 
131  @pipeBase.timeMethod
132  def runDataRef(self, butler, dataRefList):
133  """!Make a skymap from the bounds of the given set of calexps.
134 
135  @param[in] butler data butler used to save the SkyMap
136  @param[in] dataRefList dataRefs of calexps used to determine the size and pointing of the SkyMap
137  @return a pipeBase Struct containing:
138  - skyMap: the constructed SkyMap
139  """
140  self.log.info("Extracting bounding boxes of %d images" % len(dataRefList))
141  points = []
142  for dataRef in dataRefList:
143  if not dataRef.datasetExists("calexp"):
144  self.log.warn("CalExp for %s does not exist: ignoring" % (dataRef.dataId,))
145  continue
146  md = dataRef.get("calexp_md", immediate=True)
147  wcs = afwGeom.makeSkyWcs(md)
148  # nb: don't need to worry about xy0 because Exposure saves Wcs with CRPIX shifted by (-x0, -y0).
149  boxI = afwImage.bboxFromMetadata(md)
150  boxD = afwGeom.Box2D(boxI)
151  points.extend(wcs.pixelToSky(corner).getVector() for corner in boxD.getCorners())
152  if len(points) == 0:
153  raise RuntimeError("No data found from which to compute convex hull")
154  self.log.info("Computing spherical convex hull")
155  polygon = lsst.sphgeom.ConvexPolygon.convexHull(points)
156  if polygon is None:
157  raise RuntimeError(
158  "Failed to compute convex hull of the vertices of all calexp bounding boxes; "
159  "they may not be hemispherical."
160  )
161  circle = polygon.getBoundingCircle()
162 
163  datasetName = self.config.coaddName + "Coadd_skyMap"
164 
165  skyMapConfig = DiscreteSkyMap.ConfigClass()
166  if self.config.doAppend and butler.datasetExists(datasetName):
167  oldSkyMap = butler.get(datasetName, immediate=True)
168  if not isinstance(oldSkyMap.config, DiscreteSkyMap.ConfigClass):
169  raise TypeError("Cannot append to existing non-discrete skymap")
170  compareLog = []
171  if not self.config.skyMap.compare(oldSkyMap.config, output=compareLog.append):
172  raise ValueError("Cannot append to existing skymap - configurations differ:", *compareLog)
173  skyMapConfig.raList.extend(oldSkyMap.config.raList)
174  skyMapConfig.decList.extend(oldSkyMap.config.decList)
175  skyMapConfig.radiusList.extend(oldSkyMap.config.radiusList)
176  skyMapConfig.update(**self.config.skyMap.toDict())
177  circleCenter = lsst.sphgeom.LonLat(circle.getCenter())
178  skyMapConfig.raList.append(circleCenter[0].asDegrees())
179  skyMapConfig.decList.append(circleCenter[1].asDegrees())
180  circleRadiusDeg = circle.getOpeningAngle().asDegrees()
181  skyMapConfig.radiusList.append(circleRadiusDeg + self.config.borderSize)
182  skyMap = DiscreteSkyMap(skyMapConfig)
183 
184  for tractInfo in skyMap:
185  wcs = tractInfo.getWcs()
186  posBox = afwGeom.Box2D(tractInfo.getBBox())
187  pixelPosList = (
188  posBox.getMin(),
189  afwGeom.Point2D(posBox.getMaxX(), posBox.getMinY()),
190  posBox.getMax(),
191  afwGeom.Point2D(posBox.getMinX(), posBox.getMaxY()),
192  )
193  skyPosList = [wcs.pixelToSky(pos).getPosition(afwGeom.degrees) for pos in pixelPosList]
194  posStrList = ["(%0.3f, %0.3f)" % tuple(skyPos) for skyPos in skyPosList]
195  self.log.info("tract %s has corners %s (RA, Dec deg) and %s x %s patches" %
196  (tractInfo.getId(), ", ".join(posStrList),
197  tractInfo.getNumPatches()[0], tractInfo.getNumPatches()[1]))
198  if self.config.doWrite:
199  butler.put(skyMap, datasetName)
200  return pipeBase.Struct(
201  skyMap=skyMap
202  )
203 
204  def _getConfigName(self):
205  """Return None to disable saving config
206 
207  There's only one SkyMap per repository, so the config is redundant, and checking it means we can't
208  easily overwrite or append to an existing repository.
209  """
210  return None
211 
212  def _getMetadataName(self):
213  """Return None to disable saving metadata
214 
215  The metadata is not interesting, and by not saving it we can eliminate a dataset type.
216  """
217  return None
218 
219  @classmethod
220  def _makeArgumentParser(cls):
221  parser = ArgumentParser(name=cls._DefaultName)
222  parser.add_id_argument(name="--id", datasetType="calexp", help="data ID, e.g. --id visit=123 ccd=1,2")
223  return parser
A floating-point coordinate rectangle geometry.
Definition: Box.h:305
def runDataRef(self, butler, dataRefList)
Make a skymap from the bounds of the given set of calexps.
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
LonLat represents a spherical coordinate (longitude/latitude angle) pair.
Definition: LonLat.h:48
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
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:709
Make a DiscreteSkyMap in a repository, using the bounding box of a set of calexps.