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