Coverage for python/lsst/ap/association/skyBotEphemerisQuery.py: 32%

Shortcuts on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

77 statements  

1# This file is part of ap_association. 

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"""Solar System Object Query to Skybot in place of a internal Rubin solar 

23system object caching/retrieval code. 

24 

25Will compute the location for of known SSObjects within a visit. This code 

26blocks on web requests, so should not be used as part of any real-time or 

27time-sensitive system. Use in a larger pipeline at your own risk. 

28""" 

29 

30__all__ = ["SkyBotEphemerisQueryConfig", "SkyBotEphemerisQueryTask"] 

31 

32 

33from hashlib import blake2b 

34import numpy as np 

35import pandas as pd 

36import requests 

37from io import StringIO 

38 

39from lsst.daf.base import DateTime 

40import lsst.pex.config as pexConfig 

41import lsst.pipe.base as pipeBase 

42from lsst.utils.timer import timeMethod 

43 

44from lsst.pipe.base import PipelineTask, PipelineTaskConfig, PipelineTaskConnections 

45import lsst.pipe.base.connectionTypes as connTypes 

46 

47# Enforce an error for unsafe column/array value setting in pandas. 

48pd.options.mode.chained_assignment = 'raise' 

49 

50 

51class SkyBotEphemerisQueryConnections(PipelineTaskConnections, 

52 dimensions=("instrument", 

53 "visit")): 

54 visitInfos = connTypes.Input( 

55 doc="Information defining the visit on a per detector basis.", 

56 name="raw.visitInfo", 

57 storageClass="VisitInfo", 

58 dimensions=("instrument", "exposure", "detector"), 

59 deferLoad=True, 

60 multiple=True 

61 ) 

62 ssObjects = connTypes.Output( 

63 doc="Solar System objects observable in this visit retrieved from " 

64 "SkyBoy", 

65 name="visitSsObjects", 

66 storageClass="DataFrame", 

67 dimensions=("instrument", "visit"), 

68 ) 

69 

70 

71class SkyBotEphemerisQueryConfig( 

72 PipelineTaskConfig, 

73 pipelineConnections=SkyBotEphemerisQueryConnections): 

74 observerCode = pexConfig.Field( 

75 dtype=str, 

76 doc="IAU Minor Planet Center observer code for queries " 

77 "(Rubin Obs./LSST default is I11)", 

78 default='I11' 

79 ) 

80 queryRadiusDegrees = pexConfig.Field( 

81 dtype=float, 

82 doc="On sky radius for Ephemeris cone search. Also limits sky " 

83 "position error in ephemeris query. Defaults to the radius of " 

84 "Rubin Obs FoV in degrees", 

85 default=1.75) 

86 

87 

88class SkyBotEphemerisQueryTask(PipelineTask): 

89 """Tasks to query the SkyBot service and retrieve the solar system objects 

90 that are observable within the input visit. 

91 """ 

92 ConfigClass = SkyBotEphemerisQueryConfig 

93 _DefaultName = "SkyBotEphemerisQuery" 

94 

95 def runQuantum(self, butlerQC, inputRefs, outputRefs): 

96 inputs = butlerQC.get(inputRefs) 

97 inputs["visit"] = butlerQC.quantum.dataId["visit"] 

98 

99 outputs = self.run(**inputs) 

100 

101 butlerQC.put(outputs, outputRefs) 

102 

103 @timeMethod 

104 def run(self, visitInfos, visit): 

105 """Parse the information on the current visit and retrieve the 

106 observable solar system objects from SkyBot. 

107 

108 Parameters 

109 ---------- 

110 visitInfos : `list` of `lsst.daf.butler.DeferredDatasetHandle` 

111 Set of visitInfos for raws covered by this visit/exposure. We 

112 only use the first instance to retrieve the exposure boresight. 

113 visit : `int` 

114 Id number of the visit being run. 

115 

116 Returns 

117 ------- 

118 result : `lsst.pipe.base.Struct` 

119 Results struct with components: 

120 

121 - ``ssObjects``: `pandas.DataFrame` 

122 DataFrame containing name, RA/DEC position, position 

123 uncertainty, and unique Id of on sky of Solar System Objects in 

124 field of view as retrieved by SkyBot. 

125 """ 

126 # Grab the visitInfo from the raw to get the information needed on the 

127 # full visit. 

128 visitInfo = visitInfos[0].get( 

129 datasetType=self.config.connections.visitInfos) 

130 

131 # Midpoint time of the exposure in MJD 

132 expMidPointEPOCH = visitInfo.date.get(system=DateTime.JD) 

133 

134 # Boresight of the exposure on sky. 

135 expCenter = visitInfo.boresightRaDec 

136 

137 # Skybot service query 

138 skybotSsObjects = self._skybotConeSearch( 

139 expCenter, 

140 expMidPointEPOCH, 

141 self.config.queryRadiusDegrees) 

142 

143 # Add the visit as an extra column. 

144 skybotSsObjects['visitId'] = visit 

145 

146 return pipeBase.Struct( 

147 ssObjects=skybotSsObjects, 

148 ) 

149 

150 def _skybotConeSearch(self, expCenter, epochJD, queryRadius): 

151 """Query IMCCE SkyBot ephemeris service for cone search using the 

152 exposure boresight. 

153 

154 Parameters 

155 ---------- 

156 expCenter : `lsst.geom.SpherePoint` 

157 Center of Exposure RADEC [deg] 

158 epochJD : `float` 

159 Mid point time of exposure [EPOCH]. 

160 queryRadius : `float` 

161 Radius of the cone search in degrees. 

162 

163 Returns 

164 ------- 

165 dfSSO : `pandas.DataFrame` 

166 DataFrame with Solar System Object information and RA/DEC position 

167 within the visit. 

168 """ 

169 

170 fieldRA = expCenter.getRa().asDegrees() 

171 fieldDec = expCenter.getDec().asDegrees() 

172 observerMPCId = self.config.observerCode 

173 radius = queryRadius 

174 orbitUncertaintyFilter = queryRadius 

175 

176 # TODO: DM-31866 

177 q = ['http://vo.imcce.fr/webservices/skybot/skybotconesearch_query.php?'] 

178 q.append('-ra=' + str(fieldRA)) 

179 q.append('&-dec=' + str(fieldDec)) 

180 q.append('&-rd=' + str(radius)) 

181 q.append('&-ep=' + str(epochJD)) 

182 q.append('&-loc=' + observerMPCId) 

183 q.append('&-filter=' + str(orbitUncertaintyFilter)) 

184 q.append('&-objFilter=111&-refsys=EQJ2000&-output=obs&-mime=text') 

185 query = ''.join(q) 

186 

187 result = requests.request("GET", query) 

188 dfSSO = pd.read_csv(StringIO(result.text), sep='|', skiprows=2) 

189 if len(dfSSO) > 0: 

190 # Data has leading and trailing spaces hence the strip. 

191 columns = [col.strip() for col in dfSSO.columns] 

192 coldict = dict(zip(dfSSO.columns, columns)) 

193 dfSSO.rename(columns=coldict, inplace=True) 

194 # TODO: DM-31934 Replace custom conversion code with 

195 # Astropy.coordinates. 

196 dfSSO["ra"] = self._rahms2radeg(dfSSO["RA(h)"]) 

197 dfSSO["decl"] = self._decdms2decdeg(dfSSO["DE(deg)"]) 

198 # SkyBot returns a string name for the object. To store the id in 

199 # the Apdb we convert this string to an int by hashing the object 

200 # name. This is a stop gap until such a time as the Rubin 

201 # Ephemeris system exists and we create our own Ids. Use blake2b 

202 # is it can produce hashes that can fit in a 64bit int. 

203 dfSSO["ssObjectId"] = [ 

204 int(blake2b(bytes(name, "utf-8"), digest_size=7).hexdigest(), 

205 base=16) 

206 for name in dfSSO["Name"] 

207 ] 

208 else: 

209 self.log.info("No Solar System objects found for visit.") 

210 return pd.DataFrame() 

211 

212 nFound = len(dfSSO) 

213 self.log.info(f"{nFound} Solar System Objects in visit") 

214 

215 return dfSSO 

216 

217 def _decdms2decdeg(self, decdms): 

218 """Convert Declination from degrees minutes seconds to decimal degrees. 

219 

220 Parameters 

221 ---------- 

222 decdms : `list` of `str` 

223 Declination string "degrees minutes seconds" 

224 

225 Returns 

226 ------- 

227 decdeg : `numpy.ndarray`, (N,) 

228 Declination in degrees. 

229 """ 

230 decdeg = np.empty(len(decdms)) 

231 for idx, dec in enumerate(decdms): 

232 deglist = [float(d) for d in dec.split()] 

233 decdeg[idx] = deglist[0] + deglist[1]/60 + deglist[2]/3600 

234 return decdeg 

235 

236 def _rahms2radeg(self, rahms): 

237 """Convert Right Ascension from hours minutes seconds to decimal 

238 degrees. 

239 

240 Parameters 

241 ---------- 

242 rahms : `list` of `str` 

243 Declination string "hours minutes seconds" 

244 Returns 

245 ------- 

246 radeg : `numpy.ndarray`, (N,) 

247 Right Ascension in degrees 

248 """ 

249 radeg = np.empty(len(rahms)) 

250 for idx, ra in enumerate(rahms): 

251 ralist = [float(r) for r in ra.split()] 

252 radeg[idx] = (ralist[0]/24 + ralist[1]/1440 + ralist[2]/86400)*360 

253 return radeg