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

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/>.
22"""Solar System Object Query to Skybot in place of a internal Rubin solar
23system object caching/retrieval code.
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"""
30__all__ = ["SkyBotEphemerisQueryConfig", "SkyBotEphemerisQueryTask"]
33from hashlib import blake2b
34import numpy as np
35import pandas as pd
36import requests
37from io import StringIO
39from lsst.daf.base import DateTime
40import lsst.pex.config as pexConfig
41import lsst.pipe.base as pipeBase
43from lsst.pipe.base import PipelineTask, PipelineTaskConfig, PipelineTaskConnections
44import lsst.pipe.base.connectionTypes as connTypes
46# Enforce an error for unsafe column/array value setting in pandas.
47pd.options.mode.chained_assignment = 'raise'
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 )
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)
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"
94 def runQuantum(self, butlerQC, inputRefs, outputRefs):
95 inputs = butlerQC.get(inputRefs)
96 inputs["visit"] = butlerQC.quantum.dataId["visit"]
98 outputs = self.run(**inputs)
100 butlerQC.put(outputs, outputRefs)
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.
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.
115 Returns
116 -------
117 result : `lsst.pipe.base.Struct`
118 Results struct with components:
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)
130 # Midpoint time of the exposure in MJD
131 expMidPointEPOCH = visitInfo.date.get(system=DateTime.EPOCH)
133 # Boresight of the exposure on sky.
134 expCenter = visitInfo.boresightRaDec
136 # Skybot service query
137 skybotSsObjects = self._skybotConeSearch(
138 expCenter,
139 expMidPointEPOCH,
140 self.config.queryRadiusDegrees)
142 # Add the visit as an extra column.
143 skybotSsObjects['visitId'] = visit
145 return pipeBase.Struct(
146 ssObjects=skybotSsObjects,
147 )
149 def _skybotConeSearch(self, expCenter, epochJD, queryRadius):
150 """Query IMCCE SkyBot ephemeris service for cone search using the
151 exposure boresight.
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.
162 Returns
163 -------
164 dfSSO : `pandas.DataFrame`
165 DataFrame with Solar System Object information and RA/DEC position
166 within the visit.
167 """
169 fieldRA = expCenter.getRa().asDegrees()
170 fieldDec = expCenter.getDec().asDegrees()
171 observerMPCId = self.config.observerCode
172 radius = queryRadius
173 orbitUncertaintyFilter = queryRadius
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)
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()
211 nFound = len(dfSSO)
212 self.log.info(f"{nFound} Solar System Objects in visit")
214 return dfSSO
216 def _decdms2decdeg(self, decdms):
217 """Convert Declination from degrees minutes seconds to decimal degrees.
219 Parameters
220 ----------
221 decdms : `list` of `str`
222 Declination string "degrees minutes seconds"
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
235 def _rahms2radeg(self, rahms):
236 """Convert Right Ascension from hours minutes seconds to decimal
237 degrees.
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