Coverage for python/lsst/pipe/tasks/makeDiscreteSkyMap.py: 25%

108 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-07-17 08:57 +0000

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# 

22import sys 

23import traceback 

24import lsst.sphgeom 

25 

26import lsst.geom as geom 

27import lsst.pex.config as pexConfig 

28import lsst.pipe.base as pipeBase 

29from lsst.skymap import DiscreteSkyMap, BaseSkyMap 

30from lsst.pipe.base import ArgumentParser 

31 

32 

33class MakeDiscreteSkyMapConfig(pexConfig.Config): 

34 """Config for MakeDiscreteSkyMapTask 

35 """ 

36 coaddName = pexConfig.Field( 

37 doc="coadd name, e.g. deep, goodSeeing, chiSquared", 

38 dtype=str, 

39 default="deep", 

40 ) 

41 skyMap = pexConfig.ConfigField( 

42 dtype=BaseSkyMap.ConfigClass, 

43 doc="SkyMap configuration parameters, excluding position and radius" 

44 ) 

45 borderSize = pexConfig.Field( 

46 doc="additional border added to the bounding box of the calexps, in degrees", 

47 dtype=float, 

48 default=0.0 

49 ) 

50 doAppend = pexConfig.Field( 

51 doc="append another tract to an existing DiscreteSkyMap on disk, if present?", 

52 dtype=bool, 

53 default=False 

54 ) 

55 doWrite = pexConfig.Field( 

56 doc="persist the skyMap?", 

57 dtype=bool, 

58 default=True, 

59 ) 

60 

61 def setDefaults(self): 

62 self.skyMap.tractOverlap = 0.0 

63 

64 

65class MakeDiscreteSkyMapRunner(pipeBase.TaskRunner): 

66 """Run a task with all dataRefs at once, rather than one dataRef at a time. 

67 

68 Call the run method of the task using two positional arguments: 

69 - butler: data butler 

70 - dataRefList: list of all dataRefs, 

71 """ 

72 @staticmethod 

73 def getTargetList(parsedCmd): 

74 return [(parsedCmd.butler, parsedCmd.id.refList)] 

75 

76 def __call__(self, args): 

77 """ 

78 @param args Arguments for Task.run() 

79 

80 @return: 

81 - None if self.doReturnResults false 

82 - A pipe_base Struct containing these fields if self.doReturnResults true: 

83 - dataRef: the provided data reference 

84 - metadata: task metadata after execution of run 

85 - result: result returned by task run, or None if the task fails 

86 """ 

87 butler, dataRefList = args 

88 task = self.TaskClass(config=self.config, log=self.log) 

89 result = None # in case the task fails 

90 exitStatus = 0 # exit status for shell 

91 if self.doRaise: 

92 result = task.runDataRef(butler, dataRefList) 

93 else: 

94 try: 

95 result = task.runDataRef(butler, dataRefList) 

96 except Exception as e: 

97 task.log.fatal("Failed: %s", e) 

98 exitStatus = 1 

99 if not isinstance(e, pipeBase.TaskError): 

100 traceback.print_exc(file=sys.stderr) 

101 for dataRef in dataRefList: 

102 task.writeMetadata(dataRef) 

103 

104 if self.doReturnResults: 

105 return pipeBase.Struct( 

106 dataRefList=dataRefList, 

107 metadata=task.metadata, 

108 result=result, 

109 exitStatus=exitStatus, 

110 ) 

111 else: 

112 return pipeBase.Struct( 

113 exitStatus=exitStatus, 

114 ) 

115 

116 

117class MakeDiscreteSkyMapTask(pipeBase.CmdLineTask): 

118 """!Make a DiscreteSkyMap in a repository, using the bounding box of a set of calexps. 

119 

120 The command-line and run signatures and config are sufficiently different from MakeSkyMapTask 

121 that we don't inherit from it, but it is a replacement, so we use the same config/metadata names. 

122 """ 

123 ConfigClass = MakeDiscreteSkyMapConfig 

124 _DefaultName = "makeDiscreteSkyMap" 

125 RunnerClass = MakeDiscreteSkyMapRunner 

126 

127 def __init__(self, **kwargs): 

128 super().__init__(**kwargs) 

129 

130 def runDataRef(self, butler, dataRefList): 

131 """Make a skymap from the bounds of the given set of calexps using the butler. 

132 

133 Parameters 

134 ---------- 

135 butler : `lsst.daf.persistence.Butler` 

136 Gen2 data butler used to save the SkyMap 

137 dataRefList : iterable 

138 A list of Gen2 data refs of calexps used to determin the size and pointing of the SkyMap 

139 Returns 

140 ------- 

141 struct : `lsst.pipe.base.Struct` 

142 The returned struct has one attribute, ``skyMap``, which holds the returned SkyMap 

143 """ 

144 wcs_bbox_tuple_list = [] 

145 oldSkyMap = None 

146 datasetName = self.config.coaddName + "Coadd_skyMap" 

147 for dataRef in dataRefList: 

148 if not dataRef.datasetExists("calexp"): 

149 self.log.warning("CalExp for %s does not exist: ignoring", dataRef.dataId) 

150 continue 

151 wcs_bbox_tuple_list.append((dataRef.get("calexp_wcs", immediate=True), 

152 dataRef.get("calexp_bbox", immediate=True))) 

153 if self.config.doAppend and butler.datasetExists(datasetName): 

154 oldSkyMap = butler.get(datasetName, immediate=True) 

155 if not isinstance(oldSkyMap.config, DiscreteSkyMap.ConfigClass): 

156 raise TypeError("Cannot append to existing non-discrete skymap") 

157 compareLog = [] 

158 if not self.config.skyMap.compare(oldSkyMap.config, output=compareLog.append): 

159 raise ValueError("Cannot append to existing skymap - configurations differ:", *compareLog) 

160 result = self.run(wcs_bbox_tuple_list, oldSkyMap) 

161 if self.config.doWrite: 

162 butler.put(result.skyMap, datasetName) 

163 return result 

164 

165 @pipeBase.timeMethod 

166 def run(self, wcs_bbox_tuple_list, oldSkyMap=None): 

167 """Make a SkyMap from the bounds of the given set of calexp metadata. 

168 

169 Parameters 

170 ---------- 

171 wcs_bbox_tuple_list : iterable 

172 A list of tuples with each element expected to be a (Wcs, Box2I) pair 

173 oldSkyMap : `lsst.skymap.DiscreteSkyMap`, option 

174 The SkyMap to extend if appending 

175 Returns 

176 ------- 

177 struct : `lsst.pipe.base.Struct 

178 The returned struct has one attribute, ``skyMap``, which holds the returned SkyMap 

179 """ 

180 self.log.info("Extracting bounding boxes of %d images", len(wcs_bbox_tuple_list)) 

181 points = [] 

182 for wcs, boxI in wcs_bbox_tuple_list: 

183 boxD = geom.Box2D(boxI) 

184 points.extend(wcs.pixelToSky(corner).getVector() for corner in boxD.getCorners()) 

185 if len(points) == 0: 

186 raise RuntimeError("No data found from which to compute convex hull") 

187 self.log.info("Computing spherical convex hull") 

188 polygon = lsst.sphgeom.ConvexPolygon.convexHull(points) 

189 if polygon is None: 

190 raise RuntimeError( 

191 "Failed to compute convex hull of the vertices of all calexp bounding boxes; " 

192 "they may not be hemispherical." 

193 ) 

194 circle = polygon.getBoundingCircle() 

195 

196 skyMapConfig = DiscreteSkyMap.ConfigClass() 

197 if oldSkyMap: 

198 skyMapConfig.raList.extend(oldSkyMap.config.raList) 

199 skyMapConfig.decList.extend(oldSkyMap.config.decList) 

200 skyMapConfig.radiusList.extend(oldSkyMap.config.radiusList) 

201 skyMapConfig.update(**self.config.skyMap.toDict()) 

202 circleCenter = lsst.sphgeom.LonLat(circle.getCenter()) 

203 skyMapConfig.raList.append(circleCenter[0].asDegrees()) 

204 skyMapConfig.decList.append(circleCenter[1].asDegrees()) 

205 circleRadiusDeg = circle.getOpeningAngle().asDegrees() 

206 skyMapConfig.radiusList.append(circleRadiusDeg + self.config.borderSize) 

207 skyMap = DiscreteSkyMap(skyMapConfig) 

208 

209 for tractInfo in skyMap: 

210 wcs = tractInfo.getWcs() 

211 posBox = geom.Box2D(tractInfo.getBBox()) 

212 pixelPosList = ( 

213 posBox.getMin(), 

214 geom.Point2D(posBox.getMaxX(), posBox.getMinY()), 

215 posBox.getMax(), 

216 geom.Point2D(posBox.getMinX(), posBox.getMaxY()), 

217 ) 

218 skyPosList = [wcs.pixelToSky(pos).getPosition(geom.degrees) for pos in pixelPosList] 

219 posStrList = ["(%0.3f, %0.3f)" % tuple(skyPos) for skyPos in skyPosList] 

220 self.log.info("tract %s has corners %s (RA, Dec deg) and %s x %s patches", 

221 tractInfo.getId(), ", ".join(posStrList), 

222 tractInfo.getNumPatches()[0], tractInfo.getNumPatches()[1]) 

223 return pipeBase.Struct( 

224 skyMap=skyMap 

225 ) 

226 

227 def _getConfigName(self): 

228 """Return None to disable saving config 

229 

230 There's only one SkyMap per repository, so the config is redundant, and checking it means we can't 

231 easily overwrite or append to an existing repository. 

232 """ 

233 return None 

234 

235 def _getMetadataName(self): 

236 """Return None to disable saving metadata 

237 

238 The metadata is not interesting, and by not saving it we can eliminate a dataset type. 

239 """ 

240 return None 

241 

242 @classmethod 

243 def _makeArgumentParser(cls): 

244 parser = ArgumentParser(name=cls._DefaultName) 

245 parser.add_id_argument(name="--id", datasetType="calexp", help="data ID, e.g. --id visit=123 ccd=1,2") 

246 return parser