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

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 Solar System Objects in field of view as 

123 retrieved by SkyBot. The columns are as follows; for more 

124 details see 

125 https://ssp.imcce.fr/webservices/skybot/api/conesearch/#output-results 

126 

127 ``"Num"`` 

128 object number (`int`, optional) 

129 ``"Name"`` 

130 object name (`str`) 

131 ``"RA(h)"`` 

132 RA in HMS (`str`) 

133 ``"DE(deg)"`` 

134 DEC in DMS (`str`) 

135 ``"Class"`` 

136 Minor planet classification (`str`) 

137 ``"Mv"`` 

138 visual magnitude (`float`) 

139 ``"Err(arcsec)"`` 

140 position error (`float`) 

141 ``"d(arcsec)"`` 

142 distance from exposure boresight (`float`)? 

143 ``"dRA(arcsec/h)"`` 

144 proper motion in RA (`float`) 

145 ``"dDEC(arcsec/h)"`` 

146 proper motion in DEC (`float`) 

147 ``"Dg(ua)"`` 

148 geocentric distance (`float`) 

149 ``"Dh(ua)"`` 

150 heliocentric distance (`float`) 

151 ``"Phase(deg)"`` 

152 phase angle (`float`) 

153 ``"SunElong(deg)"`` 

154 solar elongation (`float`) 

155 ``"ra"`` 

156 RA in decimal degrees (`float`) 

157 ``"decl"`` 

158 DEC in decimal degrees (`float`) 

159 ``"ssObjectId"`` 

160 unique minor planet ID for internal use (`int`). Shared 

161 across catalogs; the pair ``(ssObjectId, visitId)`` is 

162 globally unique. 

163 ``"visitId"`` 

164 a copy of ``visit`` (`int`) 

165 """ 

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

167 # full visit. 

168 visitInfo = visitInfos[0].get( 

169 datasetType=self.config.connections.visitInfos) 

170 

171 # Midpoint time of the exposure in MJD 

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

173 

174 # Boresight of the exposure on sky. 

175 expCenter = visitInfo.boresightRaDec 

176 

177 # Skybot service query 

178 skybotSsObjects = self._skybotConeSearch( 

179 expCenter, 

180 expMidPointEPOCH, 

181 self.config.queryRadiusDegrees) 

182 

183 # Add the visit as an extra column. 

184 skybotSsObjects['visitId'] = visit 

185 

186 return pipeBase.Struct( 

187 ssObjects=skybotSsObjects, 

188 ) 

189 

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

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

192 exposure boresight. 

193 

194 Parameters 

195 ---------- 

196 expCenter : `lsst.geom.SpherePoint` 

197 Center of Exposure RADEC [deg] 

198 epochJD : `float` 

199 Mid point time of exposure [EPOCH]. 

200 queryRadius : `float` 

201 Radius of the cone search in degrees. 

202 

203 Returns 

204 ------- 

205 dfSSO : `pandas.DataFrame` 

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

207 within the visit. 

208 """ 

209 

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

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

212 observerMPCId = self.config.observerCode 

213 radius = queryRadius 

214 orbitUncertaintyFilter = queryRadius 

215 

216 # TODO: DM-31866 

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

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

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

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

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

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

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

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

225 query = ''.join(q) 

226 

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

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

229 if len(dfSSO) > 0: 

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

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

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

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

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

235 # Astropy.coordinates. 

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

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

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

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

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

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

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

243 dfSSO["ssObjectId"] = [ 

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

245 base=16) 

246 for name in dfSSO["Name"] 

247 ] 

248 else: 

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

250 return pd.DataFrame() 

251 

252 nFound = len(dfSSO) 

253 self.log.info("%d Solar System Objects in visit", nFound) 

254 

255 return dfSSO 

256 

257 def _decdms2decdeg(self, decdms): 

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

259 

260 Parameters 

261 ---------- 

262 decdms : `list` of `str` 

263 Declination string "degrees minutes seconds" 

264 

265 Returns 

266 ------- 

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

268 Declination in degrees. 

269 """ 

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

271 for idx, dec in enumerate(decdms): 

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

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

274 return decdeg 

275 

276 def _rahms2radeg(self, rahms): 

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

278 degrees. 

279 

280 Parameters 

281 ---------- 

282 rahms : `list` of `str` 

283 Declination string "hours minutes seconds" 

284 Returns 

285 ------- 

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

287 Right Ascension in degrees 

288 """ 

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

290 for idx, ra in enumerate(rahms): 

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

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

293 return radeg