Hide keyboard shortcuts

Hot-keys 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

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 

42 

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

44import lsst.pipe.base.connectionTypes as connTypes 

45 

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

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

48 

49 

50class SkyBotEphemerisQueryConnections(PipelineTaskConnections, 

51 dimensions=("instrument", 

52 "visit")): 

53 visitInfos = connTypes.Input( 

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

55 name="raw.visitInfo", 

56 storageClass="VisitInfo", 

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

58 deferLoad=True, 

59 multiple=True 

60 ) 

61 ssObjects = connTypes.Output( 

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

63 "SkyBoy", 

64 name="visitSsObjects", 

65 storageClass="DataFrame", 

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

67 ) 

68 

69 

70class SkyBotEphemerisQueryConfig( 

71 PipelineTaskConfig, 

72 pipelineConnections=SkyBotEphemerisQueryConnections): 

73 observerCode = pexConfig.Field( 

74 dtype=str, 

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

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

77 default='I11' 

78 ) 

79 queryRadiusDegrees = pexConfig.Field( 

80 dtype=float, 

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

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

83 "Rubin Obs FoV in degrees", 

84 default=1.75) 

85 

86 

87class SkyBotEphemerisQueryTask(PipelineTask): 

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

89 that are observable within the input visit. 

90 """ 

91 ConfigClass = SkyBotEphemerisQueryConfig 

92 _DefaultName = "SkyBotEphemerisQuery" 

93 

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

95 inputs = butlerQC.get(inputRefs) 

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

97 

98 outputs = self.run(**inputs) 

99 

100 butlerQC.put(outputs, outputRefs) 

101 

102 @pipeBase.timeMethod 

103 def run(self, visitInfos, visit): 

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

105 observable solar system objects from SkyBot. 

106 

107 Parameters 

108 ---------- 

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

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

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

112 visit : `int` 

113 Id number of the visit being run. 

114 

115 Returns 

116 ------- 

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

118 Results struct with components: 

119 

120 - ``ssObjects``: `pandas.DataFrame` 

121 DataFrame containing name, RA/DEC position, position 

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

123 field of view as retrieved by SkyBot. 

124 """ 

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

126 # full visit. 

127 visitInfo = visitInfos[0].get( 

128 datasetType=self.config.connections.visitInfos) 

129 

130 # Midpoint time of the exposure in MJD 

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

132 

133 # Boresight of the exposure on sky. 

134 expCenter = visitInfo.boresightRaDec 

135 

136 # Skybot service query 

137 skybotSsObjects = self._skybotConeSearch( 

138 expCenter, 

139 expMidPointEPOCH, 

140 self.config.queryRadiusDegrees) 

141 

142 # Add the visit as an extra column. 

143 skybotSsObjects['visitId'] = visit 

144 

145 return pipeBase.Struct( 

146 ssObjects=skybotSsObjects, 

147 ) 

148 

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

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

151 exposure boresight. 

152 

153 Parameters 

154 ---------- 

155 expCenter : `lsst.geom.SpherePoint` 

156 Center of Exposure RADEC [deg] 

157 epochJD : `float` 

158 Mid point time of exposure [EPOCH]. 

159 queryRadius : `float` 

160 Radius of the cone search in degrees. 

161 

162 Returns 

163 ------- 

164 dfSSO : `pandas.DataFrame` 

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

166 within the visit. 

167 """ 

168 

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

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

171 observerMPCId = self.config.observerCode 

172 radius = queryRadius 

173 orbitUncertaintyFilter = queryRadius 

174 

175 # TODO: DM-31866 

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

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

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

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

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

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

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

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

184 query = ''.join(q) 

185 

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

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

188 if len(dfSSO) > 0: 

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

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

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

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

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

194 # Astropy.coordinates. 

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

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

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

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

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

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

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

202 dfSSO["ssObjectId"] = [ 

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

204 base=16) 

205 for name in dfSSO["Name"] 

206 ] 

207 else: 

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

209 return pd.DataFrame() 

210 

211 nFound = len(dfSSO) 

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

213 

214 return dfSSO 

215 

216 def _decdms2decdeg(self, decdms): 

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

218 

219 Parameters 

220 ---------- 

221 decdms : `list` of `str` 

222 Declination string "degrees minutes seconds" 

223 

224 Returns 

225 ------- 

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

227 Declination in degrees. 

228 """ 

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

230 for idx, dec in enumerate(decdms): 

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

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

233 return decdeg 

234 

235 def _rahms2radeg(self, rahms): 

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

237 degrees. 

238 

239 Parameters 

240 ---------- 

241 rahms : `list` of `str` 

242 Declination string "hours minutes seconds" 

243 Returns 

244 ------- 

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

246 Right Ascension in degrees 

247 """ 

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

249 for idx, ra in enumerate(rahms): 

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

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

252 return radeg