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

66 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-02-08 03:39 -0800

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 pandas as pd 

35import requests 

36from io import StringIO 

37 

38from astropy.coordinates import Angle 

39from astropy import units as u 

40 

41from lsst.daf.base import DateTime 

42import lsst.pex.config as pexConfig 

43import lsst.pipe.base as pipeBase 

44from lsst.utils.timer import timeMethod 

45 

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

47import lsst.pipe.base.connectionTypes as connTypes 

48 

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

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

51 

52 

53class SkyBotEphemerisQueryConnections(PipelineTaskConnections, 

54 dimensions=("instrument", 

55 "visit")): 

56 visitInfos = connTypes.Input( 

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

58 name="raw.visitInfo", 

59 storageClass="VisitInfo", 

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

61 deferLoad=True, 

62 multiple=True 

63 ) 

64 ssObjects = connTypes.Output( 

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

66 "SkyBoy", 

67 name="visitSsObjects", 

68 storageClass="DataFrame", 

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

70 ) 

71 

72 

73class SkyBotEphemerisQueryConfig( 

74 PipelineTaskConfig, 

75 pipelineConnections=SkyBotEphemerisQueryConnections): 

76 observerCode = pexConfig.Field( 

77 dtype=str, 

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

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

80 default='I11' 

81 ) 

82 queryRadiusDegrees = pexConfig.Field( 

83 dtype=float, 

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

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

86 "Rubin Obs FoV in degrees", 

87 default=1.75) 

88 

89 

90class SkyBotEphemerisQueryTask(PipelineTask): 

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

92 that are observable within the input visit. 

93 """ 

94 ConfigClass = SkyBotEphemerisQueryConfig 

95 _DefaultName = "SkyBotEphemerisQuery" 

96 

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

98 inputs = butlerQC.get(inputRefs) 

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

100 

101 outputs = self.run(**inputs) 

102 

103 butlerQC.put(outputs, outputRefs) 

104 

105 @timeMethod 

106 def run(self, visitInfos, visit): 

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

108 observable solar system objects from SkyBot. 

109 

110 Parameters 

111 ---------- 

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

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

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

115 visit : `int` 

116 Id number of the visit being run. 

117 

118 Returns 

119 ------- 

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

121 Results struct with components: 

122 

123 - ``ssObjects``: `pandas.DataFrame` 

124 DataFrame containing Solar System Objects in field of view as 

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

126 details see 

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

128 

129 ``"Num"`` 

130 object number (`int`, optional) 

131 ``"Name"`` 

132 object name (`str`) 

133 ``"RA(h)"`` 

134 RA in HMS (`str`) 

135 ``"DE(deg)"`` 

136 DEC in DMS (`str`) 

137 ``"Class"`` 

138 Minor planet classification (`str`) 

139 ``"Mv"`` 

140 visual magnitude (`float`) 

141 ``"Err(arcsec)"`` 

142 position error (`float`) 

143 ``"d(arcsec)"`` 

144 distance from exposure boresight (`float`)? 

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

146 proper motion in RA (`float`) 

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

148 proper motion in DEC (`float`) 

149 ``"Dg(ua)"`` 

150 geocentric distance (`float`) 

151 ``"Dh(ua)"`` 

152 heliocentric distance (`float`) 

153 ``"Phase(deg)"`` 

154 phase angle (`float`) 

155 ``"SunElong(deg)"`` 

156 solar elongation (`float`) 

157 ``"ra"`` 

158 RA in decimal degrees (`float`) 

159 ``"decl"`` 

160 DEC in decimal degrees (`float`) 

161 ``"ssObjectId"`` 

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

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

164 globally unique. 

165 ``"visitId"`` 

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

167 """ 

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

169 # full visit. 

170 visitInfo = visitInfos[0].get( 

171 datasetType=self.config.connections.visitInfos) 

172 

173 # Midpoint time of the exposure in JD 

174 expMidPointEPOCH = visitInfo.date.get(system=DateTime.JD, scale=DateTime.UTC) 

175 

176 # Boresight of the exposure on sky. 

177 expCenter = visitInfo.boresightRaDec 

178 

179 # Skybot service query 

180 skybotSsObjects = self._skybotConeSearch( 

181 expCenter, 

182 expMidPointEPOCH, 

183 self.config.queryRadiusDegrees) 

184 

185 # Add the visit as an extra column. 

186 skybotSsObjects['visitId'] = visit 

187 

188 return pipeBase.Struct( 

189 ssObjects=skybotSsObjects, 

190 ) 

191 

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

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

194 exposure boresight. 

195 

196 Parameters 

197 ---------- 

198 expCenter : `lsst.geom.SpherePoint` 

199 Center of Exposure RADEC [deg] 

200 epochJD : `float` 

201 Mid point JD of exposure, in UTC [EPOCH]. 

202 queryRadius : `float` 

203 Radius of the cone search in degrees. 

204 

205 Returns 

206 ------- 

207 dfSSO : `pandas.DataFrame` 

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

209 within the visit. 

210 """ 

211 

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

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

214 observerMPCId = self.config.observerCode 

215 radius = queryRadius 

216 orbitUncertaintyFilter = queryRadius 

217 

218 # TODO: DM-31866 

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

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

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

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

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

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

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

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

227 query = ''.join(q) 

228 

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

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

231 if len(dfSSO) > 0: 

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

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

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

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

236 # Data returned in hourangle format. 

237 dfSSO["ra"] = Angle(dfSSO["RA(h)"], unit=u.hourangle).deg 

238 dfSSO["decl"] = Angle(dfSSO["DE(deg)"], unit=u.deg).deg 

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

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

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

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

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

244 dfSSO["ssObjectId"] = [ 

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

246 base=16) 

247 for name in dfSSO["Name"] 

248 ] 

249 else: 

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

251 return pd.DataFrame() 

252 

253 nFound = len(dfSSO) 

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

255 

256 return dfSSO